diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9060e29fd..245258b6f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -176,7 +176,7 @@ jobs:
- name: Prepare Hiero Solo
id: solo
- uses: hiero-ledger/hiero-solo-action@328bc84c3b00a990a151418144fd682a4eb76ea6 # v0.17
+ uses: hiero-ledger/hiero-solo-action@4d42a74e8e644a2753f3bb7a2afa429305375b14 # v0.16
with:
installMirrorNode: true
soloVersion: v0.65.0
@@ -255,8 +255,8 @@ jobs:
installMirrorNode: true
soloVersion: v0.65.0
dualMode: true
- hieroVersion: v0.68.0
- mirrorNodeVersion: v0.142.0
+ hieroVersion: v0.73.0
+ mirrorNodeVersion: v0.153.0
- name: Build SDK
run: ./gradlew assemble
diff --git a/examples/src/main/java/com/hedera/hashgraph/sdk/examples/RegisteredNodeLifeCycleExample.java b/examples/src/main/java/com/hedera/hashgraph/sdk/examples/RegisteredNodeLifeCycleExample.java
new file mode 100644
index 000000000..9cd49b841
--- /dev/null
+++ b/examples/src/main/java/com/hedera/hashgraph/sdk/examples/RegisteredNodeLifeCycleExample.java
@@ -0,0 +1,173 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk.examples;
+
+import com.hedera.hashgraph.sdk.AccountId;
+import com.hedera.hashgraph.sdk.BlockNodeApi;
+import com.hedera.hashgraph.sdk.BlockNodeServiceEndpoint;
+import com.hedera.hashgraph.sdk.Client;
+import com.hedera.hashgraph.sdk.NodeUpdateTransaction;
+import com.hedera.hashgraph.sdk.PrivateKey;
+import com.hedera.hashgraph.sdk.RegisteredNodeCreateTransaction;
+import com.hedera.hashgraph.sdk.RegisteredNodeDeleteTransaction;
+import com.hedera.hashgraph.sdk.RegisteredNodeUpdateTransaction;
+import com.hedera.hashgraph.sdk.TransactionReceipt;
+import com.hedera.hashgraph.sdk.TransactionResponse;
+import com.hedera.hashgraph.sdk.logger.LogLevel;
+import com.hedera.hashgraph.sdk.logger.Logger;
+import io.github.cdimascio.dotenv.Dotenv;
+import java.util.List;
+import java.util.Objects;
+
+public class RegisteredNodeLifeCycleExample {
+ /*
+ * See .env.sample in the examples folder root for how to specify values below
+ * or set environment variables with the same names.
+ */
+
+ /**
+ * Operator's account ID.
+ * Used to sign and pay for operations on Hedera.
+ */
+ private static final AccountId OPERATOR_ID =
+ AccountId.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_ID")));
+
+ /**
+ * Operator's private key.
+ */
+ private static final PrivateKey OPERATOR_KEY =
+ PrivateKey.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_KEY")));
+
+ /**
+ * HEDERA_NETWORK defaults to testnet if not specified in dotenv file.
+ * Network can be: localhost, testnet, previewnet or mainnet.
+ */
+ private static final String HEDERA_NETWORK = Dotenv.load().get("HEDERA_NETWORK", "testnet");
+
+ /**
+ * SDK_LOG_LEVEL defaults to SILENT if not specified in dotenv file.
+ * Log levels can be: TRACE, DEBUG, INFO, WARN, ERROR, SILENT.
+ *
+ * Important pre-requisite: set simple logger log level to same level as the SDK_LOG_LEVEL,
+ * for example via VM options: -Dorg.slf4j.simpleLogger.log.org.hiero=trace
+ */
+ private static final String SDK_LOG_LEVEL = Dotenv.load().get("SDK_LOG_LEVEL", "SILENT");
+
+ public static void main(String[] args) throws Exception {
+ System.out.println("Registered Node Lifecycle Example Start!");
+
+ /*
+ * Step 0:
+ * Create and configure the SDK Client.
+ */
+ Client client = ClientHelper.forName(HEDERA_NETWORK);
+ // All generated transactions will be paid by this account and signed by this key.
+ client.setOperator(OPERATOR_ID, OPERATOR_KEY);
+ // Attach logger to the SDK Client.
+ client.setLogger(new Logger(LogLevel.valueOf(SDK_LOG_LEVEL)));
+
+ /*
+ * Step 1:
+ * Generate an admin key pair and configure a BlockNodeServiceEndpoint
+ * for use in the RegisterNodeTransaction.
+ */
+ PrivateKey adminKey = PrivateKey.generateED25519();
+ BlockNodeServiceEndpoint initialEndpoint = new BlockNodeServiceEndpoint()
+ .setIpAddress(new byte[] {127, 0, 0, 1})
+ .setPort(443)
+ .setRequiresTls(true)
+ .addEndpointApi(BlockNodeApi.SUBSCRIBE_STREAM);
+
+ /*
+ * Step 2:
+ * Create Registered Node.
+ */
+ RegisteredNodeCreateTransaction registeredNodeCreateTx = new RegisteredNodeCreateTransaction()
+ .setDescription("My Block Node")
+ .setAdminKey(adminKey)
+ .addServiceEndpoint(initialEndpoint)
+ .freezeWith(client)
+ .sign(adminKey);
+
+ System.out.println("Creating Registered Node...");
+ TransactionResponse registeredNodeCreateTxResponse = registeredNodeCreateTx.execute(client);
+ TransactionReceipt registeredNodeCreateTxReceipt = registeredNodeCreateTxResponse.getReceipt(client);
+
+ if (registeredNodeCreateTxReceipt.registeredNodeId <= 0) {
+ throw new Exception("RegisteredNodeCreate transaction receipt was missing registeredNodeId. (Fail)");
+ }
+
+ long registeredNodeId = registeredNodeCreateTxReceipt.registeredNodeId;
+
+ /*
+ * Step 3:
+ * Execute a RegisteredNodeAddressBookQuery to verify the newly created
+ * registered node appears in the RegisteredNodeAddressBook.
+ */
+ System.out.println("Skipping registered node address book query because mirror node API is not available...");
+
+ /*
+ * Step 4:
+ * Update the RegisteredNode with new Block Node endpoint.
+ */
+ BlockNodeServiceEndpoint updateEndpoint = new BlockNodeServiceEndpoint()
+ .setDomainName("block-node.example.com")
+ .setPort(443)
+ .setRequiresTls(true)
+ .addEndpointApi(BlockNodeApi.STATUS);
+
+ RegisteredNodeUpdateTransaction registeredNodeUpdateTx = new RegisteredNodeUpdateTransaction()
+ .setRegisteredNodeId(registeredNodeId)
+ .setDescription("My Updated Block Node")
+ .setServiceEndpoints(List.of(initialEndpoint, updateEndpoint))
+ .freezeWith(client)
+ .sign(adminKey);
+
+ System.out.println("Updating Registered Node...");
+ TransactionResponse registeredNodeUpdateTxResponse = registeredNodeUpdateTx.execute(client);
+ registeredNodeUpdateTxResponse.getReceipt(client);
+
+ /*
+ * Step 5:
+ * Add the registeredNodeId as associatedRegisteredNodes to a Node.
+ */
+
+ NodeUpdateTransaction associateTx = new NodeUpdateTransaction()
+ .setNodeId(0)
+ .addAssociatedRegisteredNode(registeredNodeId)
+ .freezeWith(client);
+
+ System.out.println("Associating registered node " + registeredNodeId + " with consensus node...");
+ TransactionResponse associateTxResponse = associateTx.execute(client);
+ associateTxResponse.getReceipt(client);
+
+ /*
+ * Step 6:
+ * Remove the registeredNodeId as associatedRegisteredNodes from a Node.
+ */
+
+ NodeUpdateTransaction disassociateTx = new NodeUpdateTransaction()
+ .setNodeId(0)
+ .setAssociatedRegisteredNodes(List.of()) // Empty list clear associated registeredNode
+ .freezeWith(client);
+
+ System.out.println("Disassociating registered node " + registeredNodeId + " with consensus node...");
+ disassociateTx.execute(client);
+ associateTxResponse.getReceipt(client);
+
+ /*
+ * Step 7:
+ * Delete the Registered Node.
+ */
+ System.out.println("Deleting Registered Node...");
+ new RegisteredNodeDeleteTransaction()
+ .setRegisteredNodeId(registeredNodeCreateTxReceipt.registeredNodeId)
+ .freezeWith(client)
+ .sign(adminKey)
+ .execute(client)
+ .getReceipt(client);
+
+ client.close();
+
+ System.out.println("Registered Node Lifecycle Example Complete!");
+ }
+}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/BlockNodeApi.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/BlockNodeApi.java
new file mode 100644
index 000000000..7e75ab514
--- /dev/null
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/BlockNodeApi.java
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint.BlockNodeEndpoint;
+
+/**
+ * An enumeration of well-known block node endpoint APIs.
+ */
+public enum BlockNodeApi {
+ /**
+ * Any other API type associated with a block node.
+ */
+ OTHER(BlockNodeEndpoint.BlockNodeApi.OTHER),
+
+ /**
+ * The Block Node Status API.
+ */
+ STATUS(BlockNodeEndpoint.BlockNodeApi.STATUS),
+
+ /**
+ * The Block Node Publish API.
+ */
+ PUBLISH(BlockNodeEndpoint.BlockNodeApi.PUBLISH),
+
+ /**
+ * The Block Node Subscribe Stream API.
+ */
+ SUBSCRIBE_STREAM(BlockNodeEndpoint.BlockNodeApi.SUBSCRIBE_STREAM),
+
+ /**
+ * The Block Node State Proof API.
+ */
+ STATE_PROOF(BlockNodeEndpoint.BlockNodeApi.STATE_PROOF);
+
+ final BlockNodeEndpoint.BlockNodeApi code;
+
+ BlockNodeApi(BlockNodeEndpoint.BlockNodeApi code) {
+ this.code = code;
+ }
+
+ static BlockNodeApi valueOf(BlockNodeEndpoint.BlockNodeApi code) {
+ return switch (code) {
+ case OTHER -> OTHER;
+ case STATUS -> STATUS;
+ case PUBLISH -> PUBLISH;
+ case SUBSCRIBE_STREAM -> SUBSCRIBE_STREAM;
+ case STATE_PROOF -> STATE_PROOF;
+ default -> throw new IllegalArgumentException("Unhandled BlockNodeApi code");
+ };
+ }
+
+ @Override
+ public String toString() {
+ return code.name();
+ }
+}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/BlockNodeServiceEndpoint.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/BlockNodeServiceEndpoint.java
new file mode 100644
index 000000000..07fbaf1f8
--- /dev/null
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/BlockNodeServiceEndpoint.java
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.protobuf.ByteString;
+import com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Represent a Registered Block Node
+ */
+public class BlockNodeServiceEndpoint extends RegisteredServiceEndpointBase {
+ /**
+ * An indicator of what API this endpoint supports.
+ */
+ private List endpointApis = new ArrayList<>();
+
+ /**
+ * Constructor.
+ */
+ public BlockNodeServiceEndpoint() {}
+
+ /**
+ * Returns the list of APIs supported by this endpoint.
+ *
+ * @return the list of supported block node APIs
+ */
+ public List getEndpointApis() {
+ return endpointApis;
+ }
+
+ /**
+ * Sets the list of APIs supported by this endpoint.
+ *
+ * @param endpointApis the list of APIs to support; must not be null
+ * @return {@code this}
+ */
+ public BlockNodeServiceEndpoint setEndpointApis(List endpointApis) {
+ Objects.requireNonNull(endpointApis, "endpointApis must not be null");
+ this.endpointApis = new ArrayList<>(endpointApis);
+ return this;
+ }
+
+ /**
+ * Adds a single API to the list of supported APIs for this endpoint.
+ *
+ * @param endpointApi the API to add; must not be null
+ * @return {@code this}
+ */
+ public BlockNodeServiceEndpoint addEndpointApi(BlockNodeApi endpointApi) {
+ Objects.requireNonNull(endpointApi, "endpointApi must not be null");
+ this.endpointApis.add(endpointApi);
+ return this;
+ }
+
+ /**
+ * Create a BlockNodeServiceEndpoint object from protobuf
+ *
+ * @param serviceEndpoint the protobuf object
+ * @return the new instance of BlockNodeServiceEndpoint
+ */
+ static BlockNodeServiceEndpoint fromProtobuf(
+ com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint serviceEndpoint) {
+ Objects.requireNonNull(serviceEndpoint, "serviceEndpoint must not be null");
+
+ var blockNodeEndpoint = new BlockNodeServiceEndpoint()
+ .setPort(serviceEndpoint.getPort())
+ .setRequiresTls(serviceEndpoint.getRequiresTls());
+
+ if (serviceEndpoint.hasBlockNode()) {
+ for (var apiProto : serviceEndpoint.getBlockNode().getEndpointApiList()) {
+ blockNodeEndpoint.addEndpointApi(BlockNodeApi.valueOf(apiProto));
+ }
+ }
+
+ if (serviceEndpoint.hasIpAddress()) {
+ blockNodeEndpoint.setIpAddress(serviceEndpoint.getIpAddress().toByteArray());
+ }
+
+ if (serviceEndpoint.hasDomainName()) {
+ blockNodeEndpoint.setDomainName(serviceEndpoint.getDomainName());
+ }
+
+ return blockNodeEndpoint;
+ }
+
+ @Override
+ com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint toProtobuf() {
+ if (ipAddress == null && domainName == null) {
+ throw new IllegalArgumentException(
+ "RegisterServiceEndpoint must define either an ipAddress or domainName");
+ }
+
+ var blockNodeBuilder = RegisteredServiceEndpoint.BlockNodeEndpoint.newBuilder()
+ .addAllEndpointApi(endpointApis.stream().map(api -> api.code).toList());
+
+ var registeredServiceEndpoint = com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint.newBuilder()
+ .setPort(port)
+ .setRequiresTls(requiresTls)
+ .setBlockNode(blockNodeBuilder);
+
+ if (ipAddress != null) {
+ registeredServiceEndpoint.setIpAddress(ByteString.copyFrom(this.ipAddress));
+ }
+
+ if (domainName != null) {
+ registeredServiceEndpoint.setDomainName(this.domainName);
+ }
+
+ return registeredServiceEndpoint.build();
+ }
+
+ /**
+ * Parses BlockNodeServiceEndpoint from the type-specific JSON object the MirrorNode.
+ *
+ * @param json the json containing block node specific data
+ * @return {@code this}
+ */
+ static BlockNodeServiceEndpoint fromJson(JsonObject json) {
+ Objects.requireNonNull(json, "json must not be null");
+
+ List apis = new ArrayList<>();
+ if (json.has("endpoint_apis")) {
+ for (JsonElement api : json.getAsJsonArray("endpoint_apis")) {
+ apis.add(api.getAsString());
+ }
+ }
+
+ return new BlockNodeServiceEndpoint()
+ .setEndpointApis(
+ apis.stream().map(a -> BlockNodeApi.valueOf(a)).collect(Collectors.toUnmodifiableList()));
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper().add("endpointApis", endpointApis).toString();
+ }
+}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/GeneralServiceEndpoint.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/GeneralServiceEndpoint.java
new file mode 100644
index 000000000..c19578bbd
--- /dev/null
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/GeneralServiceEndpoint.java
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import com.google.gson.JsonObject;
+import com.google.protobuf.ByteString;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Represents a general-purpose service endpoint.
+ */
+public class GeneralServiceEndpoint extends RegisteredServiceEndpointBase {
+ /**
+ * A short description of the service provided.
+ */
+ @Nullable
+ private String description;
+
+ /**
+ * Constructor.
+ */
+ public GeneralServiceEndpoint() {}
+
+ /**
+ * Returns the description of the service provided by this endpoint.
+ *
+ * @return the service description, or null if not set
+ */
+ public @Nullable String getDescription() {
+ return this.description;
+ }
+
+ /**
+ * Sets a short description of the service provided.
+ * This value MUST NOT exceed 100 bytes when encoded as UTF-8.
+ *
+ * @param description a short description of the service
+ * @return {@code this}
+ */
+ public GeneralServiceEndpoint setDescription(String description) {
+ this.description = description;
+ return this;
+ }
+
+ static GeneralServiceEndpoint fromProtobuf(
+ com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint serviceEndpoint) {
+ Objects.requireNonNull(serviceEndpoint, "serviceEndpoint must not be null");
+
+ var generalEndpoint = new GeneralServiceEndpoint()
+ .setPort(serviceEndpoint.getPort())
+ .setRequiresTls(serviceEndpoint.getRequiresTls())
+ .setDescription(serviceEndpoint.getGeneralService().getDescription());
+
+ if (serviceEndpoint.hasIpAddress()) {
+ generalEndpoint.setIpAddress(serviceEndpoint.getIpAddress().toByteArray());
+ }
+
+ if (serviceEndpoint.hasDomainName()) {
+ generalEndpoint.setDomainName(serviceEndpoint.getDomainName());
+ }
+
+ return generalEndpoint;
+ }
+
+ /**
+ * Parses GeneralServiceEndpoint from the type-specific JSON object the MirrorNode.
+ *
+ * @param json the json containing general service specific data
+ * @return {@code this}
+ */
+ static GeneralServiceEndpoint fromJson(JsonObject json) {
+ Objects.requireNonNull(json, "json must not be null");
+
+ String description = json.has("description") && !json.get("description").isJsonNull()
+ ? json.get("description").getAsString()
+ : null;
+
+ return new GeneralServiceEndpoint().setDescription(description);
+ }
+
+ @Override
+ com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint toProtobuf() {
+ if (ipAddress == null && domainName == null) {
+ throw new IllegalArgumentException(
+ "RegisterServiceEndpoint must define either an ipAddress or domainName");
+ }
+
+ var generalService =
+ com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint.GeneralServiceEndpoint.newBuilder();
+ if (description != null) {
+ generalService.setDescription(description);
+ }
+
+ var registeredServiceEndpoint = com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint.newBuilder()
+ .setPort(port)
+ .setRequiresTls(requiresTls)
+ .setGeneralService(generalService);
+
+ if (ipAddress != null) {
+ registeredServiceEndpoint.setIpAddress(ByteString.copyFrom(this.ipAddress));
+ }
+
+ if (domainName != null) {
+ registeredServiceEndpoint.setDomainName(this.domainName);
+ }
+
+ return registeredServiceEndpoint.build();
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper().add("description", description).toString();
+ }
+}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/MirrorNodeServiceEndpoint.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/MirrorNodeServiceEndpoint.java
new file mode 100644
index 000000000..906c999b5
--- /dev/null
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/MirrorNodeServiceEndpoint.java
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import com.google.gson.JsonObject;
+import com.google.protobuf.ByteString;
+import com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint;
+import java.util.Objects;
+
+/**
+ * Represent a Registered Mirror Node
+ */
+public class MirrorNodeServiceEndpoint extends RegisteredServiceEndpointBase {
+ /**
+ * Constructor.
+ *
+ */
+ public MirrorNodeServiceEndpoint() {}
+
+ /**
+ * Create a MirrorNodeServiceEndpoint object from protobuf
+ *
+ * @param serviceEndpoint the protobuf object
+ * @return the new instance of MirrorNodeServiceEndpoint
+ */
+ static MirrorNodeServiceEndpoint fromProtobuf(
+ com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint serviceEndpoint) {
+ Objects.requireNonNull(serviceEndpoint, "serviceEndpoint must not be null");
+
+ var mirrorNode = new MirrorNodeServiceEndpoint()
+ .setPort(serviceEndpoint.getPort())
+ .setRequiresTls(serviceEndpoint.getRequiresTls());
+
+ if (serviceEndpoint.hasIpAddress()) {
+ mirrorNode.setIpAddress(serviceEndpoint.getIpAddress().toByteArray());
+ }
+
+ if (serviceEndpoint.hasDomainName()) {
+ mirrorNode.setDomainName(serviceEndpoint.getDomainName());
+ }
+
+ return mirrorNode;
+ }
+
+ @Override
+ com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint toProtobuf() {
+ var registeredServiceEndpoint = com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint.newBuilder()
+ .setPort(port)
+ .setRequiresTls(requiresTls)
+ .setMirrorNode(RegisteredServiceEndpoint.MirrorNodeEndpoint.newBuilder());
+
+ if (ipAddress != null) {
+ registeredServiceEndpoint.setIpAddress(ByteString.copyFrom(ipAddress));
+ }
+
+ if (domainName != null) {
+ registeredServiceEndpoint.setDomainName(domainName);
+ }
+
+ return registeredServiceEndpoint.build();
+ }
+
+ /**
+ * Parses MirrorNodeServiceEndpoint from the type-specific JSON object the MirrorNode.
+ *
+ * @param json the json containing mirror node specific data
+ * @return {@code this}
+ */
+ static MirrorNodeServiceEndpoint fromJson(JsonObject json) {
+ Objects.requireNonNull(json, "json must not be null");
+ return new MirrorNodeServiceEndpoint();
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper().toString();
+ }
+}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeCreateTransaction.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeCreateTransaction.java
index 6257d4c09..888753510 100644
--- a/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeCreateTransaction.java
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeCreateTransaction.java
@@ -66,6 +66,8 @@ public class NodeCreateTransaction extends Transaction {
@Nullable
private Endpoint grpcWebProxyEndpoint = null;
+ private List associatedRegisteredNodes = new ArrayList<>();
+
/**
* Constructor.
*/
@@ -396,6 +398,53 @@ public NodeCreateTransaction setGrpcWebProxyEndpoint(@Nullable Endpoint grpcWebP
return this;
}
+ /**
+ * Get a list of registered nodes operated by the same entity as this node.
+ * @return {@code List} the list of associated registered node.
+ */
+ public List getAssociatedRegisteredNodes() {
+ return associatedRegisteredNodes;
+ }
+
+ /**
+ * Set a list of registered nodes operated by the same entity as this node.
+ * This value may contain a list of "registered nodes" (as described in
+ * HIP-1137) that are operated by the same entity that operates this
+ * consensus node.
+ *
+ * This field is OPTIONAL and MAY be empty.
+ * This field MUST NOT contain more than twenty(20) entries.
+ * Every entry in this list MUST be a valid `registered_node_id` for a
+ * current registered node.
+ *
+ * @param associatedRegisteredNodes list of associated registered node.
+ * @return {@code this}
+ */
+ public NodeCreateTransaction setAssociatedRegisteredNodes(List associatedRegisteredNodes) {
+ requireNotFrozen();
+ Objects.requireNonNull(associatedRegisteredNodes);
+ if (associatedRegisteredNodes.size() > 20) {
+ throw new IllegalArgumentException("associatedRegisteredNodes must not contain more than 20 entries");
+ }
+
+ this.associatedRegisteredNodes = new ArrayList<>(associatedRegisteredNodes);
+ return this;
+ }
+
+ /**
+ * Add a registered nodes operated by the same entity as this node.
+ * @param associatedRegisteredNode the associated registered node.
+ * @return {@code this}
+ */
+ public NodeCreateTransaction addAssociatedRegisteredNode(long associatedRegisteredNode) {
+ requireNotFrozen();
+ if (associatedRegisteredNodes.size() >= 20) {
+ throw new IllegalArgumentException("associatedRegisteredNodes must not contain more than 20 entries");
+ }
+ associatedRegisteredNodes.add(associatedRegisteredNode);
+ return this;
+ }
+
/**
* Build the transaction body.
*
@@ -438,6 +487,10 @@ NodeCreateTransactionBody.Builder build() {
builder.addServiceEndpoint(serviceEndpoint.toProtobuf());
}
+ for (Long associatedRegisteredNode : associatedRegisteredNodes) {
+ builder.addAssociatedRegisteredNode(associatedRegisteredNode);
+ }
+
if (gossipCaCertificate != null) {
builder.setGossipCaCertificate(ByteString.copyFrom(gossipCaCertificate));
}
@@ -481,6 +534,10 @@ void initFromTransactionBody() {
serviceEndpoints.add(Endpoint.fromProtobuf(serviceEndpoint));
}
+ for (var associatedRegisteredNode : body.getAssociatedRegisteredNodeList()) {
+ associatedRegisteredNodes.add(associatedRegisteredNode);
+ }
+
var protobufGossipCert = body.getGossipCaCertificate();
gossipCaCertificate = protobufGossipCert.equals(ByteString.empty()) ? null : protobufGossipCert.toByteArray();
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeUpdateTransaction.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeUpdateTransaction.java
index eadf30aa3..a1aba466e 100644
--- a/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeUpdateTransaction.java
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeUpdateTransaction.java
@@ -7,6 +7,7 @@
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.StringValue;
import com.hedera.hashgraph.sdk.proto.AddressBookServiceGrpc;
+import com.hedera.hashgraph.sdk.proto.AssociatedRegisteredNodeList;
import com.hedera.hashgraph.sdk.proto.NodeUpdateTransactionBody;
import com.hedera.hashgraph.sdk.proto.SchedulableTransactionBody;
import com.hedera.hashgraph.sdk.proto.TransactionBody;
@@ -67,6 +68,9 @@ public class NodeUpdateTransaction extends Transaction {
@Nullable
private Endpoint grpcWebProxyEndpoint = null;
+ @Nullable
+ private List associatedRegisteredNodes = null;
+
/**
* Constructor.
*/
@@ -316,6 +320,16 @@ public NodeUpdateTransaction addServiceEndpoint(Endpoint serviceEndpoint) {
return this;
}
+ /**
+ * Clear serviceEndpoint lists.
+ * @return {@code this}
+ */
+ public NodeUpdateTransaction clearServiceEndpoint() {
+ requireNotFrozen();
+ serviceEndpoints.clear();
+ return this;
+ }
+
/**
* Extract the certificate used to sign gossip events.
* @return the DER encoding of the certificate presented.
@@ -455,6 +469,59 @@ public NodeUpdateTransaction setGrpcWebProxyEndpoint(@Nullable Endpoint grpcWebP
return this;
}
+ /**
+ * Get a list of registered nodes operated by the same entity as this node.
+ * @return {@code List} the list of associated registered node.
+ */
+ public List getAssociatedRegisteredNodes() {
+ return associatedRegisteredNodes;
+ }
+
+ /**
+ * Set a list of registered nodes operated by the same entity as this node.
+ * This value may contain a list of "registered nodes" (as described in
+ * HIP-1137) that are operated by the same entity that operates this
+ * consensus node.
+ *
+ * This field is OPTIONAL and MAY be empty.
+ * This field MUST NOT contain more than twenty(20) entries.
+ * Every entry in this list MUST be a valid `registered_node_id` for a
+ * current registered node.
+ *
+ * @param associatedRegisteredNodes list of associated registered node.
+ * @return {@code this}
+ * @throws IllegalArgumentException if the list is empty or contains more than 8 endpoints
+ */
+ public NodeUpdateTransaction setAssociatedRegisteredNodes(List associatedRegisteredNodes) {
+ requireNotFrozen();
+ Objects.requireNonNull(associatedRegisteredNodes);
+ if (associatedRegisteredNodes.size() > 20) {
+ throw new IllegalArgumentException("associatedRegisteredNodes must not contain more than 20 entries");
+ }
+
+ this.associatedRegisteredNodes = new ArrayList<>(associatedRegisteredNodes);
+ return this;
+ }
+
+ /**
+ * Add a registered nodes operated by the same entity as this node.
+ * @param associatedRegisteredNode the associated registered node.
+ * @return {@code this}
+ */
+ public NodeUpdateTransaction addAssociatedRegisteredNode(long associatedRegisteredNode) {
+ requireNotFrozen();
+ if (associatedRegisteredNodes == null) {
+ associatedRegisteredNodes = new ArrayList<>();
+ }
+
+ if (associatedRegisteredNodes.size() >= 20) {
+ throw new IllegalArgumentException("associatedRegisteredNodes must not contain more than 20 entries");
+ }
+
+ associatedRegisteredNodes.add(associatedRegisteredNode);
+ return this;
+ }
+
/**
* Build the transaction body.
*
@@ -483,6 +550,12 @@ NodeUpdateTransactionBody.Builder build() {
builder.addServiceEndpoint(serviceEndpoint.toProtobuf());
}
+ if (associatedRegisteredNodes != null) {
+ builder.setAssociatedRegisteredNodeList(AssociatedRegisteredNodeList.newBuilder()
+ .addAllAssociatedRegisteredNode(associatedRegisteredNodes)
+ .build());
+ }
+
if (gossipCaCertificate != null) {
builder.setGossipCaCertificate(BytesValue.of(ByteString.copyFrom(gossipCaCertificate)));
}
@@ -549,6 +622,13 @@ void initFromTransactionBody() {
if (body.hasGrpcProxyEndpoint()) {
grpcWebProxyEndpoint = Endpoint.fromProtobuf(body.getGrpcProxyEndpoint());
}
+
+ if (body.hasAssociatedRegisteredNodeList()) {
+ associatedRegisteredNodes = new ArrayList<>();
+ for (long id : body.getAssociatedRegisteredNodeList().getAssociatedRegisteredNodeList()) {
+ associatedRegisteredNodes.add(id);
+ }
+ }
}
@Override
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNode.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNode.java
new file mode 100644
index 000000000..19207e6dd
--- /dev/null
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNode.java
@@ -0,0 +1,160 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import com.google.common.base.MoreObjects;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.protobuf.InvalidProtocolBufferException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nonnegative;
+
+/**
+ * Class representing single registered node in the network state.
+ * Each registered node in the network state SHALL represent a single
+ * non-consensus node that is registered on the network.
+ * Registered node identifiers SHALL only be unique within a single
+ * realm and shard combination.
+ */
+public class RegisteredNode {
+ /**
+ * A registered node identifier.
+ */
+ @Nonnegative
+ public final long registeredNodeId;
+
+ /**
+ * An administrative key controlled by the node operator.
+ */
+ public final Key adminKey;
+
+ /**
+ * A short description of the node.
+ */
+ public final String description;
+
+ /**
+ * A list of service endpoints for client calls.
+ */
+ public final List serviceEndpoints;
+
+ /**
+ * Constructor.
+ *
+ * @param registeredNodeId the registered node identifier.
+ * @param adminKey the admin key.
+ * @param description the description of the node.
+ * @param serviceEndpoint the list of service endpoints.
+ */
+ RegisteredNode(
+ long registeredNodeId, Key adminKey, String description, List serviceEndpoint) {
+ this.registeredNodeId = registeredNodeId;
+ this.adminKey = adminKey;
+ this.description = description;
+ this.serviceEndpoints = Collections.unmodifiableList(serviceEndpoint);
+ }
+
+ /**
+ * Extract the registeredNode from the protobuf.
+ *
+ * @param registeredNode the protobuf
+ * @return {@code this} the contract object
+ */
+ static RegisteredNode fromProtobuf(com.hedera.hashgraph.sdk.proto.RegisteredNode registeredNode) {
+ Objects.requireNonNull(registeredNode, "registeredNode cannot be null");
+ var registerNodeId = registeredNode.getRegisteredNodeId();
+ var adminKey = Key.fromProtobufKey(registeredNode.getAdminKey());
+ var description = registeredNode.getDescription();
+
+ var serviceEndpoint = registeredNode.getServiceEndpointList().stream()
+ .map(s -> RegisteredServiceEndpoint.fromProtobuf(s))
+ .toList();
+
+ return new RegisteredNode(registerNodeId, adminKey, description, serviceEndpoint);
+ }
+
+ /**
+ * Parses a single node entry from the Mirror Node 'registered_nodes' array.
+ *
+ * @param json the json containing specific data for registered node
+ * @return {@code this}
+ */
+ static RegisteredNode fromJson(JsonObject json) {
+ long id = json.get("registered_node_id").getAsLong();
+ String description = json.get("description").getAsString();
+ PublicKey adminKey = parseJsonKey(json.get("admin_key").getAsJsonObject());
+
+ List endpoints = new ArrayList<>();
+ for (JsonElement endpoint : json.getAsJsonArray("service_endpoints")) {
+ endpoints.add(RegisteredServiceEndpoint.fromJson(endpoint.getAsJsonObject()));
+ }
+
+ return new RegisteredNode(id, adminKey, description, endpoints);
+ }
+
+ /**
+ * Parses the admin key from the JSON representation.
+ */
+ private static PublicKey parseJsonKey(JsonObject adminKey) {
+ Objects.requireNonNull(adminKey, "adminKey must not be null");
+ String type = adminKey.get("_type").getAsString() != null
+ ? adminKey.get("_type").getAsString()
+ : "";
+
+ String key = adminKey.get("key").getAsString();
+ return switch (type) {
+ case "ED25519" -> PublicKey.fromStringED25519(key);
+ case "ECDSA_SECP256K1" -> PublicKey.fromStringECDSA(key);
+ default -> PublicKey.fromString(key);
+ };
+ }
+
+ /**
+ * Extract the registeredNode from a byte array.
+ *
+ * @param bytes the byte array
+ * @return {@code RegisteredNode} the extracted registeredNode
+ * @throws InvalidProtocolBufferException when there is an issue with the protobuf
+ */
+ public static RegisteredNode fromBytes(byte[] bytes) throws InvalidProtocolBufferException {
+ return fromProtobuf(com.hedera.hashgraph.sdk.proto.RegisteredNode.parseFrom(bytes).toBuilder()
+ .build());
+ }
+
+ /**
+ * Build the protobuf.
+ * @return {@code this} the protobuf representation
+ */
+ com.hedera.hashgraph.sdk.proto.RegisteredNode toProtobuf() {
+ var registeredNode = com.hedera.hashgraph.sdk.proto.RegisteredNode.newBuilder()
+ .setRegisteredNodeId(registeredNodeId)
+ .setAdminKey(adminKey.toProtobufKey())
+ .setDescription(description);
+
+ for (RegisteredServiceEndpoint serviceEndpoint : serviceEndpoints) {
+ registeredNode.addServiceEndpoint(serviceEndpoint.toProtobuf());
+ }
+
+ return registeredNode.build();
+ }
+
+ /**
+ * Create a byte array representation.
+ * @return {@code byte[]} the byte array representation
+ */
+ public byte[] toBytes() {
+ return toProtobuf().toByteArray();
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("registeredNodeId", registeredNodeId)
+ .add("adminKey", adminKey)
+ .add("description", description)
+ .add("serviceEndpoints", serviceEndpoints)
+ .toString();
+ }
+}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeAddressBook.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeAddressBook.java
new file mode 100644
index 000000000..cc53f09bf
--- /dev/null
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeAddressBook.java
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import java.util.List;
+
+/**
+ * Collection of RegisteredNode objects.
+ */
+public class RegisteredNodeAddressBook {
+ public final List registeredNodes;
+
+ /**
+ * Constructor.
+ *
+ * @param registeredNodes list of RegisterNode
+ */
+ RegisteredNodeAddressBook(List registeredNodes) {
+ this.registeredNodes = registeredNodes;
+ }
+
+ /**
+ * Build RegisterNodeAddressBook from protobuf.
+ *
+ * @param registeredNodes the list of RegisterNode protobuf representation.
+ * @return {@code RegisterNodeAddressBook} new object of RegisterNodeAddressBook.
+ */
+ static RegisteredNodeAddressBook fromProtobuf(List registeredNodes) {
+ return new RegisteredNodeAddressBook(
+ registeredNodes.stream().map(RegisteredNode::fromProtobuf).toList());
+ }
+}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeAddressBookQuery.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeAddressBookQuery.java
new file mode 100644
index 000000000..b0dbc89b8
--- /dev/null
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeAddressBookQuery.java
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import static com.hedera.hashgraph.sdk.EntityIdHelper.performQueryToMirrorNodeAsync;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Query the mirror node for the RegisteredAddressBook.
+ */
+public class RegisteredNodeAddressBookQuery {
+ private long registeredNodeId;
+
+ /**
+ * Sets the ID of the registered node to retrieve.
+ *
+ * @param registeredNodeId The unique identifier of the node.
+ * @return {@code this}
+ */
+ public RegisteredNodeAddressBookQuery setRegisteredNodeId(long registeredNodeId) {
+ this.registeredNodeId = registeredNodeId;
+ return this;
+ }
+
+ /**
+ * Returns the set registered node ID.
+ *
+ * @return The registered node ID.
+ */
+ public long getRegisteredNodeId() {
+ return registeredNodeId;
+ }
+
+ /**
+ * Executes the query with the user supplied client
+ *
+ * @param client The Client instance to perform the operation with
+ * @return The registeredAddressBook
+ * @throws ExecutionException
+ * @throws InterruptedException
+ */
+ public RegisteredNodeAddressBook execute(Client client) throws ExecutionException, InterruptedException {
+ String json = executeMirrorNodeRequest(client).get();
+ System.out.println(json);
+ return parseRegisterNodeAddressBook(json);
+ }
+
+ CompletableFuture executeMirrorNodeRequest(Client client) {
+ Objects.requireNonNull(client, "client must not be null");
+ String apiEndpoint = "/network/registered-nodes?registerednode.id=" + registeredNodeId;
+ String baseUrl = client.getMirrorRestBaseUrl();
+
+ // For localhost registered node calls, override to use port 8084 unless system property overrides
+ if (baseUrl.contains("localhost:5551") || baseUrl.contains("127.0.0.1:5551")) {
+ String registeredNodePort = System.getProperty("hedera.mirror.registerednode.port");
+ if (registeredNodePort != null && !registeredNodePort.isEmpty()) {
+ baseUrl = baseUrl.replace(":5551", ":" + registeredNodePort);
+ } else {
+ baseUrl = baseUrl.replace(":5551", ":8084");
+ }
+ }
+
+ return performQueryToMirrorNodeAsync(baseUrl, apiEndpoint, null).exceptionally(ex -> {
+ client.getLogger().error("Error while performing post request to Mirror Node: " + ex.getMessage());
+ throw new CompletionException(ex);
+ });
+ }
+
+ /**
+ * Converts the Mirror Node JSON response to {@link RegisteredNodeAddressBook}.
+ */
+ private RegisteredNodeAddressBook parseRegisterNodeAddressBook(String json) {
+ List registeredNodes = new ArrayList<>();
+
+ JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
+ JsonArray registeredNodesJSON = jsonObject.getAsJsonArray("registered_nodes");
+
+ for (JsonElement node : registeredNodesJSON) {
+ registeredNodes.add(RegisteredNode.fromJson(node.getAsJsonObject()));
+ }
+
+ return new RegisteredNodeAddressBook(registeredNodes);
+ }
+}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeCreateTransaction.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeCreateTransaction.java
new file mode 100644
index 000000000..cb153fee3
--- /dev/null
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeCreateTransaction.java
@@ -0,0 +1,243 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.hedera.hashgraph.sdk.proto.AddressBookServiceGrpc;
+import com.hedera.hashgraph.sdk.proto.RegisteredNodeCreateTransactionBody;
+import com.hedera.hashgraph.sdk.proto.SchedulableTransactionBody;
+import com.hedera.hashgraph.sdk.proto.TransactionBody;
+import com.hedera.hashgraph.sdk.proto.TransactionResponse;
+import io.grpc.MethodDescriptor;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * A transaction to create a new registered node in the network
+ * address book.
+ *
+ * This transaction, once complete, SHALL add a new registered node to the
+ * network state.
+ * The new registered node SHALL be visible and discoverable upon
+ * completion of this transaction.
+ */
+public class RegisteredNodeCreateTransaction extends Transaction {
+ private Key adminKey;
+ private String description = "";
+ private List serviceEndpoints = new ArrayList<>();
+
+ /**
+ * Constructor.
+ */
+ public RegisteredNodeCreateTransaction() {}
+
+ /**
+ * Constructor.
+ *
+ * @param txs Compound list of transaction id's list of (AccountId, Transaction) records
+ * @throws InvalidProtocolBufferException when there is an issue with the protobuf
+ */
+ RegisteredNodeCreateTransaction(
+ LinkedHashMap> txs)
+ throws InvalidProtocolBufferException {
+ super(txs);
+ initFromTransactionBody();
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param txBody protobuf TransactionBody
+ */
+ RegisteredNodeCreateTransaction(com.hedera.hashgraph.sdk.proto.TransactionBody txBody) {
+ super(txBody);
+ initFromTransactionBody();
+ }
+
+ /**
+ * Get administrative key controlled by the node operator.
+ * @return {@code Key} the admin key
+ */
+ public Key getAdminKey() {
+ return adminKey;
+ }
+
+ /**
+ * Set administrative key controlled by the node operator.
+ *
+ * This key MUST sign this transaction.
+ * This key MUST sign each transaction to update this node.
+ * This field MUST contain a valid `Key` value.
+ * This field is REQUIRED.
+ *
+ * @param adminKey the admin key for the registered node.
+ * @return {@code this}
+ */
+ public RegisteredNodeCreateTransaction setAdminKey(Key adminKey) {
+ this.requireNotFrozen();
+ this.adminKey = adminKey;
+ return this;
+ }
+
+ /**
+ * Get short description of the node.
+ * @return {@code String} the node's description
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * A short description of the node.
+ *
+ * This value, if set, MUST NOT exceed 100 bytes when encoded as UTF-8.
+ * This field is OPTIONAL.
+ *
+ * @param description The string to be set as description for the node.
+ * @return {@code this}
+ */
+ public RegisteredNodeCreateTransaction setDescription(@Nullable String description) {
+ this.requireNotFrozen();
+ if (description == null) {
+ this.description = "";
+ return this;
+ }
+
+ if (description.getBytes(StandardCharsets.UTF_8).length > 100) {
+ throw new IllegalArgumentException("description must not exceed 100 bytes when UTF-8 encoded");
+ }
+ this.description = description;
+ return this;
+ }
+
+ /**
+ * Get list of service endpoints for client calls.
+ * @return {@code List} list of service endpoints
+ */
+ public List getServiceEndpoints() {
+ return serviceEndpoints;
+ }
+
+ /**
+ * A list of service endpoints for client calls.
+ *
+ * These endpoints SHALL represent the published endpoints to which
+ * clients may submit requests.
+ * Endpoints in this list MAY supply either IP address or FQDN, but MUST
+ * NOT supply both values for the same endpoint.
+ * Multiple endpoints in this list MAY resolve to the same interface.
+ * One Registered Node MAY expose endpoints for multiple service types.
+ * This list MUST NOT be empty.
+ * This list MUST NOT contain more than `50` entries.
+ *
+ * @param serviceEndpoints the list of service endpoints for the client calls.
+ * @return {@code this}
+ */
+ public RegisteredNodeCreateTransaction setServiceEndpoints(List serviceEndpoints) {
+ this.requireNotFrozen();
+ Objects.requireNonNull(serviceEndpoints, "serviceEndpoints cannot be null");
+
+ if (serviceEndpoints.isEmpty()) {
+ throw new IllegalArgumentException("serviceEndpoints list must not be empty.");
+ }
+ if (serviceEndpoints.size() > 50) {
+ throw new IllegalArgumentException("serviceEndpoints must not contain more than 50 entries");
+ }
+
+ for (RegisteredServiceEndpoint serviceEndpoint : serviceEndpoints) {
+ RegisteredServiceEndpoint.validateNoIpAndDomain(serviceEndpoint);
+ }
+
+ this.serviceEndpoints = new ArrayList<>(serviceEndpoints);
+ return this;
+ }
+
+ /**
+ * Add a service endpoint for the client calls.
+ * @param serviceEndpoint the service endpoint
+ * @return {@code this}
+ */
+ public RegisteredNodeCreateTransaction addServiceEndpoint(RegisteredServiceEndpoint serviceEndpoint) {
+ requireNotFrozen();
+ if (serviceEndpoints.size() >= 50) {
+ throw new IllegalArgumentException("serviceEndpoints must not contain more than 50 entries");
+ }
+
+ RegisteredServiceEndpoint.validateNoIpAndDomain(serviceEndpoint);
+ serviceEndpoints.add(serviceEndpoint);
+ return this;
+ }
+
+ /**
+ * Build the transaction body.
+ * @return {@link com.hedera.hashgraph.sdk.proto.RegisteredNodeCreateTransactionBody}
+ */
+ RegisteredNodeCreateTransactionBody.Builder build() {
+ var builder = RegisteredNodeCreateTransactionBody.newBuilder().setDescription(description);
+
+ if (adminKey != null) {
+ builder.setAdminKey(adminKey.toProtobufKey());
+ }
+
+ for (RegisteredServiceEndpoint serviceEndpoint : serviceEndpoints) {
+ builder.addServiceEndpoint(serviceEndpoint.toProtobuf());
+ }
+
+ return builder;
+ }
+
+ /**
+ * Initialize from the transaction body.
+ */
+ void initFromTransactionBody() {
+ var body = sourceTransactionBody.getRegisteredNodeCreate();
+
+ if (body.hasAdminKey()) {
+ adminKey = Key.fromProtobufKey(body.getAdminKey());
+ }
+
+ description = body.getDescription();
+
+ serviceEndpoints.clear();
+ for (com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint serviceEndpoint : body.getServiceEndpointList()) {
+ serviceEndpoints.add(RegisteredServiceEndpoint.fromProtobuf(serviceEndpoint));
+ }
+ }
+
+ @Override
+ void validateChecksums(Client client) throws BadEntityIdException {}
+
+ @Override
+ MethodDescriptor getMethodDescriptor() {
+ return AddressBookServiceGrpc.getCreateRegisteredNodeMethod();
+ }
+
+ @Override
+ void onFreeze(TransactionBody.Builder bodyBuilder) {
+ bodyBuilder.setRegisteredNodeCreate(build());
+ }
+
+ @Override
+ void onScheduled(SchedulableTransactionBody.Builder scheduled) {
+ scheduled.setRegisteredNodeCreate(build());
+ }
+
+ /**
+ * Freeze this transaction with the given client.
+ *
+ * @param client the client to freeze with
+ * @return this transaction
+ * @throws IllegalStateException if adminKey is not set
+ */
+ @Override
+ public RegisteredNodeCreateTransaction freezeWith(@Nullable Client client) {
+ if (adminKey == null) {
+ throw new IllegalStateException(
+ "RegisteredNodeCreateTransaction: 'adminKey' must be explicitly set before calling freeze().");
+ }
+ return super.freezeWith(client);
+ }
+}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeDeleteTransaction.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeDeleteTransaction.java
new file mode 100644
index 000000000..1a0a03d56
--- /dev/null
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeDeleteTransaction.java
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.hedera.hashgraph.sdk.proto.AddressBookServiceGrpc;
+import com.hedera.hashgraph.sdk.proto.RegisteredNodeDeleteTransactionBody;
+import com.hedera.hashgraph.sdk.proto.SchedulableTransactionBody;
+import com.hedera.hashgraph.sdk.proto.TransactionBody;
+import com.hedera.hashgraph.sdk.proto.TransactionResponse;
+import io.grpc.MethodDescriptor;
+import java.util.LinkedHashMap;
+import javax.annotation.Nullable;
+
+/**
+ * A transaction to delete a registered node from the network
+ * address book.
+ *
+ * This transaction, once complete, SHALL remove the identified registered
+ * node from the network state.
+ * This transaction MUST be signed by the existing entry `admin_key` or
+ * authorized by the Hiero network governance structure.
+ */
+public class RegisteredNodeDeleteTransaction extends Transaction {
+ private Long registeredNodeId;
+
+ /**
+ * Constructor.
+ */
+ public RegisteredNodeDeleteTransaction() {}
+
+ /**
+ * Constructor.
+ *
+ * @param txs Compound list of transaction id's list of (AccountId, Transaction) records
+ * @throws InvalidProtocolBufferException when there is an issue with the protobuf
+ */
+ RegisteredNodeDeleteTransaction(
+ LinkedHashMap> txs)
+ throws InvalidProtocolBufferException {
+ super(txs);
+ initFromTransactionBody();
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param txBody protobuf TransactionBody
+ */
+ RegisteredNodeDeleteTransaction(TransactionBody txBody) {
+ super(txBody);
+ initFromTransactionBody();
+ }
+
+ /**
+ * Get registered node identifier in the network state.
+ * @return the registered node id
+ * @throws IllegalStateException when register node id is not being set
+ */
+ public long getRegisteredNodeId() {
+ if (registeredNodeId == null) {
+ throw new IllegalStateException("RegisteredNodeDeleteTransaction: 'registeredNodeId' has not been set");
+ }
+
+ return registeredNodeId;
+ }
+
+ /**
+ * SET registered node identifier in the network state.
+ *
+ * The node identified MUST exist in the registered address book.
+ * The node identified MUST NOT be deleted.
+ * This value is REQUIRED.
+ *
+ * @param registeredNodeId the registered node identifier.
+ * @return {@code this}
+ * @throws IllegalArgumentException if registeredNodeId is negative
+ */
+ public RegisteredNodeDeleteTransaction setRegisteredNodeId(long registeredNodeId) {
+ this.requireNotFrozen();
+ if (registeredNodeId < 0) {
+ throw new IllegalArgumentException(
+ "RegisteredNodeDeleteTransaction: 'registeredNodeId' must be non-negative");
+ }
+ this.registeredNodeId = registeredNodeId;
+ return this;
+ }
+
+ /**
+ * Build the transaction body.
+ * @return {@link com.hedera.hashgraph.sdk.proto.RegisteredNodeDeleteTransactionBody}
+ */
+ RegisteredNodeDeleteTransactionBody.Builder build() {
+ var builder = RegisteredNodeDeleteTransactionBody.newBuilder();
+ if (registeredNodeId != null) {
+ builder.setRegisteredNodeId(registeredNodeId);
+ }
+ return builder;
+ }
+
+ /**
+ * Initialize from the transaction body.
+ */
+ void initFromTransactionBody() {
+ var body = sourceTransactionBody.getRegisteredNodeDelete();
+ registeredNodeId = body.getRegisteredNodeId();
+ }
+
+ @Override
+ void validateChecksums(Client client) throws BadEntityIdException {
+ // no-op
+ }
+
+ @Override
+ void onFreeze(TransactionBody.Builder bodyBuilder) {
+ bodyBuilder.setRegisteredNodeDelete(build());
+ }
+
+ @Override
+ void onScheduled(SchedulableTransactionBody.Builder scheduled) {
+ scheduled.setRegisteredNodeDelete(build());
+ }
+
+ @Override
+ MethodDescriptor getMethodDescriptor() {
+ return AddressBookServiceGrpc.getDeleteRegisteredNodeMethod();
+ }
+
+ /**
+ * Freeze this transaction with the given client.
+ *
+ * @param client the client to freeze with
+ * @return this transaction
+ * @throws IllegalStateException if registeredNodeId is not set
+ */
+ @Override
+ public RegisteredNodeDeleteTransaction freezeWith(@Nullable Client client) {
+ if (registeredNodeId == null) {
+ throw new IllegalStateException(
+ "RegisteredNodeDeleteTransaction: 'registeredNodeId' must be explicitly set before calling freeze().");
+ }
+ return super.freezeWith(client);
+ }
+}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeUpdateTransaction.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeUpdateTransaction.java
new file mode 100644
index 000000000..c800436d2
--- /dev/null
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredNodeUpdateTransaction.java
@@ -0,0 +1,288 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.StringValue;
+import com.hedera.hashgraph.sdk.proto.AddressBookServiceGrpc;
+import com.hedera.hashgraph.sdk.proto.RegisteredNodeUpdateTransactionBody;
+import com.hedera.hashgraph.sdk.proto.SchedulableTransactionBody;
+import com.hedera.hashgraph.sdk.proto.TransactionBody;
+import com.hedera.hashgraph.sdk.proto.TransactionResponse;
+import io.grpc.MethodDescriptor;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * A transaction to update an existing registered node in the network
+ * address book.
+ *
+ * This transaction, once complete, SHALL modify the identified registered
+ * node state as requested.
+ */
+public class RegisteredNodeUpdateTransaction extends Transaction {
+ private Long registeredNodeId;
+
+ @Nullable
+ private Key adminKey;
+
+ @Nullable
+ private String description;
+
+ private List serviceEndpoints = new ArrayList<>();
+
+ /**
+ * Constructor.
+ */
+ public RegisteredNodeUpdateTransaction() {}
+
+ /**
+ * Constructor.
+ *
+ * @param txs Compound list of transaction id's list of (AccountId, Transaction) records
+ * @throws InvalidProtocolBufferException when there is an issue with the protobuf
+ */
+ RegisteredNodeUpdateTransaction(
+ LinkedHashMap> txs)
+ throws InvalidProtocolBufferException {
+ super(txs);
+ initFromTransactionBody();
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param txBody protobuf TransactionBody
+ */
+ RegisteredNodeUpdateTransaction(com.hedera.hashgraph.sdk.proto.TransactionBody txBody) {
+ super(txBody);
+ initFromTransactionBody();
+ }
+
+ /**
+ * Get registered node identifier in the network state.
+ * @return the registered node id
+ * @throws IllegalStateException when register node id is not being set
+ */
+ public long getRegisteredNodeId() {
+ if (registeredNodeId == null) {
+ throw new IllegalStateException("RegisteredNodeUpdateTransaction: 'registeredNodeId' has not been set");
+ }
+
+ return registeredNodeId;
+ }
+
+ /**
+ * Set registered node identifier in the network state.
+ *
+ * The node identified MUST exist in the registered address book.
+ * The node identified MUST NOT be deleted.
+ * This value is REQUIRED.
+ *
+ * @param registeredNodeId the registered node identifier.
+ * @return {@code this}
+ * @throws IllegalArgumentException if registeredNodeId is negative
+ */
+ public RegisteredNodeUpdateTransaction setRegisteredNodeId(long registeredNodeId) {
+ this.requireNotFrozen();
+ if (registeredNodeId < 0) {
+ throw new IllegalArgumentException(
+ "RegisteredNodeDeleteTransaction: 'registeredNodeId' must be non-negative");
+ }
+ this.registeredNodeId = registeredNodeId;
+ return this;
+ }
+
+ /**
+ * Get administrative key controlled by the node operator.
+ * @return {@code Key} the admin key
+ */
+ public @Nullable Key getAdminKey() {
+ return adminKey;
+ }
+
+ /**
+ * Set administrative key controlled by the node operator.
+ *
+ * This key MUST sign this transaction.
+ * This key MUST sign each transaction to update this node.
+ * This field MUST contain a valid `Key` value.
+ * This field is REQUIRED.
+ *
+ * @param adminKey the admin key for the registered node.
+ * @return {@code this}
+ */
+ public RegisteredNodeUpdateTransaction setAdminKey(@Nullable Key adminKey) {
+ this.requireNotFrozen();
+ this.adminKey = adminKey;
+ return this;
+ }
+
+ /**
+ * Get short description of the node.
+ * @return {@code String} the node's description
+ */
+ public @Nullable String getDescription() {
+ return description;
+ }
+
+ /**
+ * A short description of the node.
+ *
+ * This value, if set, MUST NOT exceed 100 bytes when encoded as UTF-8.
+ * This field is OPTIONAL.
+ *
+ * @param description The string to be set as description for the node.
+ * @return {@code this}
+ */
+ public RegisteredNodeUpdateTransaction setDescription(@Nullable String description) {
+ this.requireNotFrozen();
+ if (description != null && description.getBytes(StandardCharsets.UTF_8).length > 100) {
+ throw new IllegalArgumentException("description must not exceed 100 bytes when UTF-8 encoded");
+ }
+ this.description = description;
+ return this;
+ }
+
+ /**
+ * Get list of service endpoints for client calls.
+ * @return {@code List} list of service endpoints
+ */
+ public List getServiceEndpoints() {
+ return serviceEndpoints;
+ }
+
+ /**
+ * A list of service endpoints for client calls.
+ *
+ * These endpoints SHALL represent the published endpoints to which
+ * clients may submit requests.
+ * Endpoints in this list MAY supply either IP address or FQDN, but MUST
+ * NOT supply both values for the same endpoint.
+ * Multiple endpoints in this list MAY resolve to the same interface.
+ * One Registered Node MAY expose endpoints for multiple service types.
+ * This list MUST NOT be empty.
+ * This list MUST NOT contain more than `50` entries.
+ *
+ * @param serviceEndpoints the list of service endpoints for the client calls.
+ * @return {@code this}
+ */
+ public RegisteredNodeUpdateTransaction setServiceEndpoints(List serviceEndpoints) {
+ this.requireNotFrozen();
+ Objects.requireNonNull(serviceEndpoints, "serviceEndpoints cannot be null");
+
+ if (serviceEndpoints.isEmpty()) {
+ throw new IllegalArgumentException("ServiceEndpoints list must not be empty.");
+ }
+ if (serviceEndpoints.size() > 50) {
+ throw new IllegalArgumentException("ServiceEndpoints list must not contain more than 50 entries.");
+ }
+
+ for (RegisteredServiceEndpoint serviceEndpoint : serviceEndpoints) {
+ RegisteredServiceEndpoint.validateNoIpAndDomain(serviceEndpoint);
+ }
+
+ this.serviceEndpoints = serviceEndpoints;
+ return this;
+ }
+
+ /**
+ * Add a service endpoint for the client calls.
+ * @param serviceEndpoint the service endpoint
+ * @return {@code this}
+ */
+ public RegisteredNodeUpdateTransaction addServiceEndpoint(RegisteredServiceEndpoint serviceEndpoint) {
+ requireNotFrozen();
+ if (serviceEndpoints.size() >= 50) {
+ throw new IllegalArgumentException("serviceEndpoints must not contain more than 50 entries");
+ }
+
+ RegisteredServiceEndpoint.validateNoIpAndDomain(serviceEndpoint);
+ serviceEndpoints.add(serviceEndpoint);
+ return this;
+ }
+
+ /**
+ * Build the transaction body.
+ * @return {@link com.hedera.hashgraph.sdk.proto.RegisteredNodeUpdateTransactionBody}
+ */
+ RegisteredNodeUpdateTransactionBody.Builder build() {
+ var builder = RegisteredNodeUpdateTransactionBody.newBuilder();
+
+ if (registeredNodeId != null) {
+ builder.setRegisteredNodeId(registeredNodeId);
+ }
+
+ if (adminKey != null) {
+ builder.setAdminKey(adminKey.toProtobufKey());
+ }
+
+ if (description != null) {
+ builder.setDescription(StringValue.of(description));
+ }
+
+ for (RegisteredServiceEndpoint serviceEndpoint : serviceEndpoints) {
+ builder.addServiceEndpoint(serviceEndpoint.toProtobuf());
+ }
+
+ return builder;
+ }
+
+ /**
+ * Initialize from the transaction body.
+ */
+ void initFromTransactionBody() {
+ var body = sourceTransactionBody.getRegisteredNodeUpdate();
+ registeredNodeId = body.getRegisteredNodeId();
+
+ if (body.hasAdminKey()) {
+ adminKey = Key.fromProtobufKey(body.getAdminKey());
+ }
+
+ if (body.hasDescription()) {
+ description = body.getDescription().getValue();
+ }
+
+ serviceEndpoints.clear();
+ for (com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint serviceEndpoint : body.getServiceEndpointList()) {
+ serviceEndpoints.add(RegisteredServiceEndpoint.fromProtobuf(serviceEndpoint));
+ }
+ }
+
+ @Override
+ void onFreeze(TransactionBody.Builder bodyBuilder) {
+ bodyBuilder.setRegisteredNodeUpdate(build());
+ }
+
+ @Override
+ void onScheduled(SchedulableTransactionBody.Builder scheduled) {
+ scheduled.setRegisteredNodeUpdate(build());
+ }
+
+ @Override
+ void validateChecksums(Client client) throws BadEntityIdException {}
+
+ @Override
+ MethodDescriptor getMethodDescriptor() {
+ return AddressBookServiceGrpc.getUpdateRegisteredNodeMethod();
+ }
+
+ /**
+ * Freeze this transaction with the given client.
+ *
+ * @param client the client to freeze with
+ * @return this transaction
+ * @throws IllegalStateException if registeredNodeId is not set
+ */
+ @Override
+ public RegisteredNodeUpdateTransaction freezeWith(@Nullable Client client) {
+ if (registeredNodeId == null) {
+ throw new IllegalStateException(
+ "RegisteredNodeUpdateTransaction: 'registeredNodeId' must be explicitly set before calling freeze().");
+ }
+ return super.freezeWith(client);
+ }
+}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredServiceEndpoint.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredServiceEndpoint.java
new file mode 100644
index 000000000..2716e60b9
--- /dev/null
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredServiceEndpoint.java
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import com.google.common.base.MoreObjects;
+import com.google.gson.JsonObject;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * Abstract class representing the service endpoint published by a registered node.
+ */
+public abstract class RegisteredServiceEndpoint {
+ @Nullable
+ protected byte[] ipAddress;
+
+ @Nullable
+ protected String domainName;
+
+ protected int port;
+ protected boolean requiresTls;
+
+ RegisteredServiceEndpoint() {}
+
+ /**
+ * Get the IP address of the endpoint.
+ *
+ * @return the IP address, or null if using a domain name
+ */
+ @Nullable
+ public byte[] getIpAddress() {
+ return ipAddress != null ? ipAddress.clone() : null;
+ }
+
+ /**
+ * Get the domain name of the endpoint.
+ *
+ * @return the domain name, or null if using an IP address
+ */
+ @Nullable
+ public String getDomainName() {
+ return domainName;
+ }
+
+ /**
+ * Get the port used by this endpoint.
+ *
+ * @return the port number
+ */
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ * Check whether TLS is required for this endpoint.
+ *
+ * @return true if TLS is required
+ */
+ public boolean isRequiresTls() {
+ return requiresTls;
+ }
+
+ /**
+ * Validate that the endpoint does not contain both an IP address and a domain name.
+ *
+ * @param serviceEndpoint the endpoint to validate
+ * @throws IllegalArgumentException if both ipAddressV4 and domainName are present
+ */
+ public static void validateNoIpAndDomain(RegisteredServiceEndpoint serviceEndpoint) {
+ if (serviceEndpoint == null) {
+ return;
+ }
+ if (serviceEndpoint.getIpAddress() != null) {
+ var dn = serviceEndpoint.getDomainName();
+ if (dn != null && !dn.isEmpty()) {
+ throw new IllegalArgumentException("Service endpoint must not contain both ipAddressV4 and domainName");
+ }
+ }
+ }
+
+ static RegisteredServiceEndpoint fromProtobuf(
+ com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint serviceEndpoint) {
+ Objects.requireNonNull(serviceEndpoint, "serviceEndpoint cannot be null");
+
+ return switch (serviceEndpoint.getEndpointTypeCase()) {
+ case BLOCK_NODE -> BlockNodeServiceEndpoint.fromProtobuf(serviceEndpoint);
+ case MIRROR_NODE -> MirrorNodeServiceEndpoint.fromProtobuf(serviceEndpoint);
+ case RPC_RELAY -> RpcRelayServiceEndpoint.fromProtobuf(serviceEndpoint);
+ case GENERAL_SERVICE -> GeneralServiceEndpoint.fromProtobuf(serviceEndpoint);
+ default -> throw new IllegalArgumentException("Unable to decode registered service endpoint");
+ };
+ }
+
+ /**
+ * Parse RegisteredServiceEndpoint from MirrorNode json response `service_endpoint`
+ *
+ * @param json representing a single service endpoint entry from the Mirror Node REST API.
+ * @return {@code this}
+ */
+ static RegisteredServiceEndpoint fromJson(JsonObject json) {
+ Objects.requireNonNull(json, "serviceEndpoint must not be null");
+
+ String type = json.get("type").getAsString().toUpperCase();
+
+ int port = json.get("port").getAsInt();
+ boolean requiresTls = json.get("requires_tls").getAsBoolean();
+ String domainName = json.has("domain_name") && !json.get("domain_name").isJsonNull()
+ ? json.get("domain_name").getAsString()
+ : null;
+ byte[] ipAddress = parseIpAddress(json);
+
+ RegisteredServiceEndpointBase> registeredServiceEndpoint =
+ switch (type) {
+ case "BLOCK_NODE" -> BlockNodeServiceEndpoint.fromJson(json.getAsJsonObject("block_node"));
+ case "MIRROR_NODE" -> MirrorNodeServiceEndpoint.fromJson(json.getAsJsonObject("mirror_node"));
+ case "RPC_RELAY" -> RpcRelayServiceEndpoint.fromJson(json.getAsJsonObject("rpc_relay"));
+ case "GENERAL_SERVICE" -> GeneralServiceEndpoint.fromJson(json.getAsJsonObject("general_service"));
+ default -> throw new IllegalArgumentException("Unknown type for serviceEndpoint " + type);
+ };
+
+ return registeredServiceEndpoint
+ .setIpAddress(ipAddress)
+ .setDomainName(domainName)
+ .setPort(port)
+ .setRequiresTls(requiresTls);
+ }
+
+ /**
+ * Parse IpAddress from json response.
+ */
+ @Nullable
+ private static byte[] parseIpAddress(JsonObject json) {
+ if (json.has("ip_address") && !json.get("ip_address").isJsonNull()) {
+ String rawIp = json.get("ip_address").getAsString();
+ if (!rawIp.isEmpty()) {
+ try {
+ return InetAddress.getByName(rawIp).getAddress();
+ } catch (UnknownHostException ignored) {
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Build the protobuf.
+ *
+ * @return the protobuf representation
+ */
+ abstract com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint toProtobuf();
+
+ /**
+ * Serializes the class to ToStringHelper
+ *
+ * @return the {@link com.google.common.base.MoreObjects.ToStringHelper}
+ */
+ MoreObjects.ToStringHelper toStringHelper() {
+ return MoreObjects.toStringHelper(this)
+ .add("ipAddress", ipAddress)
+ .add("domainName", domainName)
+ .add("port", port)
+ .add("requiresTls", requiresTls);
+ }
+}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredServiceEndpointBase.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredServiceEndpointBase.java
new file mode 100644
index 000000000..1a1eadb21
--- /dev/null
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/RegisteredServiceEndpointBase.java
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import javax.annotation.Nullable;
+
+abstract class RegisteredServiceEndpointBase>
+ extends RegisteredServiceEndpoint {
+ /**
+ * Set the IP address of the endpoint.
+ *
+ * @param ipAddress the IPv4 or IPv6 address
+ * @return this endpoint
+ */
+ public T setIpAddress(@Nullable byte[] ipAddress) {
+ this.ipAddress = ipAddress;
+ // noinspection unchecked
+ return (T) this;
+ }
+
+ /**
+ * Set the domain name of the endpoint.
+ *
+ * @param domainName the fully qualified domain name
+ * @return this endpoint
+ */
+ public T setDomainName(@Nullable String domainName) {
+ this.domainName = domainName;
+ // noinspection unchecked
+ return (T) this;
+ }
+
+ /**
+ * Set the port used by this endpoint.
+ *
+ * @param port the port number
+ * @return this endpoint
+ * @throws IllegalArgumentException if the port is outside the valid range
+ */
+ public T setPort(int port) {
+ if (port < 0 || port > 65535) {
+ throw new IllegalArgumentException("Port must be in range [0, 65535]");
+ }
+ this.port = port;
+ // noinspection unchecked
+ return (T) this;
+ }
+
+ /**
+ * Set whether TLS is required for this endpoint.
+ *
+ * @param requiresTls true if TLS is required
+ * @return this endpoint
+ */
+ public T setRequiresTls(boolean requiresTls) {
+ this.requiresTls = requiresTls;
+ // noinspection unchecked
+ return (T) this;
+ }
+}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/RpcRelayServiceEndpoint.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/RpcRelayServiceEndpoint.java
new file mode 100644
index 000000000..21f3a8b30
--- /dev/null
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/RpcRelayServiceEndpoint.java
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import com.google.gson.JsonObject;
+import com.google.protobuf.ByteString;
+import com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint;
+import java.util.Objects;
+
+/**
+ * Represent a Registered Rpc Relay
+ */
+public class RpcRelayServiceEndpoint extends RegisteredServiceEndpointBase {
+ /**
+ * Constructor.
+ *
+ */
+ public RpcRelayServiceEndpoint() {}
+
+ /**
+ * Create a RpcRelayServiceEndpoint object from protobuf
+ *
+ * @param serviceEndpoint the protobuf object
+ * @return the new instance of RpcRelayServiceEndpoint
+ */
+ static RpcRelayServiceEndpoint fromProtobuf(
+ com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint serviceEndpoint) {
+ Objects.requireNonNull(serviceEndpoint, "serviceEndpoint must not be null");
+ var rpcRelay = new RpcRelayServiceEndpoint()
+ .setPort(serviceEndpoint.getPort())
+ .setRequiresTls(serviceEndpoint.getRequiresTls());
+
+ if (serviceEndpoint.hasIpAddress()) {
+ rpcRelay.setIpAddress(serviceEndpoint.getIpAddress().toByteArray());
+ }
+
+ if (serviceEndpoint.hasDomainName()) {
+ rpcRelay.setDomainName(serviceEndpoint.getDomainName());
+ }
+
+ return rpcRelay;
+ }
+
+ @Override
+ com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint toProtobuf() {
+ var registeredServiceEndpoint = com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint.newBuilder()
+ .setPort(port)
+ .setRequiresTls(requiresTls)
+ .setRpcRelay(RegisteredServiceEndpoint.RpcRelayEndpoint.newBuilder());
+
+ if (ipAddress != null) {
+ registeredServiceEndpoint.setIpAddress(ByteString.copyFrom(ipAddress));
+ }
+
+ if (domainName != null) {
+ registeredServiceEndpoint.setDomainName(domainName);
+ }
+
+ return registeredServiceEndpoint.build();
+ }
+
+ /**
+ * Parses RpcRelayEndpoint from the type-specific JSON object the MirrorNode.
+ *
+ * @param json the json containing rpc relay specific data
+ * @return {@code this}
+ */
+ static RpcRelayServiceEndpoint fromJson(JsonObject json) {
+ Objects.requireNonNull(json, "json must not be null");
+ return new RpcRelayServiceEndpoint();
+ }
+
+ @Override
+ public String toString() {
+ return toStringHelper().toString();
+ }
+}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java
index a58927922..f1c5951c2 100644
--- a/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/Status.java
@@ -2087,7 +2087,47 @@ public enum Status {
/**
* The number of hook invocations exceeds the maximum allowed per transaction.
*/
- TOO_MANY_HOOK_INVOCATIONS(ResponseCodeEnum.TOO_MANY_HOOK_INVOCATIONS);
+ TOO_MANY_HOOK_INVOCATIONS(ResponseCodeEnum.TOO_MANY_HOOK_INVOCATIONS),
+
+ /**
+ * A registered node ID is invalid or does not exist.
+ */
+ INVALID_REGISTERED_NODE_ID(ResponseCodeEnum.INVALID_REGISTERED_NODE_ID),
+
+ /**
+ * A registered service endpoint is invalid.
+ * The port is out of range, or the address field is not set.
+ */
+ INVALID_REGISTERED_ENDPOINT(ResponseCodeEnum.INVALID_REGISTERED_ENDPOINT),
+
+ /**
+ * The number of registered service endpoints exceeds the configured limit.
+ */
+ REGISTERED_ENDPOINTS_EXCEEDED_LIMIT(ResponseCodeEnum.REGISTERED_ENDPOINTS_EXCEEDED_LIMIT),
+
+ /**
+ * A registered service endpoint has an invalid address.
+ * The IP address length is not 4 (IPv4) or 16 (IPv6), or the
+ * domain name is not a valid ASCII FQDN.
+ */
+ INVALID_REGISTERED_ENDPOINT_ADDRESS(ResponseCodeEnum.INVALID_REGISTERED_ENDPOINT_ADDRESS),
+
+ /**
+ * A registered service endpoint does not specify an endpoint type.
+ * Exactly one of block_node, mirror_node, or rpc_relay MUST be set.
+ */
+ INVALID_REGISTERED_ENDPOINT_TYPE(ResponseCodeEnum.INVALID_REGISTERED_ENDPOINT_TYPE),
+
+ /**
+ * A registered node cannot be deleted because it is still associated
+ * with a consensus node via their associated registered node list.
+ */
+ REGISTERED_NODE_STILL_ASSOCIATED(ResponseCodeEnum.REGISTERED_NODE_STILL_ASSOCIATED),
+
+ /**
+ * The number of associated registered nodes exceeds the maximum allowed limit.
+ */
+ MAX_REGISTERED_NODES_EXCEEDED(ResponseCodeEnum.MAX_REGISTERED_NODES_EXCEEDED);
final ResponseCodeEnum code;
@@ -2498,6 +2538,13 @@ static Status valueOf(ResponseCodeEnum code) {
case NODE_ACCOUNT_HAS_ZERO_BALANCE -> NODE_ACCOUNT_HAS_ZERO_BALANCE;
case TRANSFER_TO_FEE_COLLECTION_ACCOUNT_NOT_ALLOWED -> TRANSFER_TO_FEE_COLLECTION_ACCOUNT_NOT_ALLOWED;
case TOO_MANY_HOOK_INVOCATIONS -> TOO_MANY_HOOK_INVOCATIONS;
+ case INVALID_REGISTERED_NODE_ID -> INVALID_REGISTERED_NODE_ID;
+ case INVALID_REGISTERED_ENDPOINT -> INVALID_REGISTERED_ENDPOINT;
+ case REGISTERED_ENDPOINTS_EXCEEDED_LIMIT -> REGISTERED_ENDPOINTS_EXCEEDED_LIMIT;
+ case INVALID_REGISTERED_ENDPOINT_ADDRESS -> INVALID_REGISTERED_ENDPOINT_ADDRESS;
+ case INVALID_REGISTERED_ENDPOINT_TYPE -> INVALID_REGISTERED_ENDPOINT_TYPE;
+ case REGISTERED_NODE_STILL_ASSOCIATED -> REGISTERED_NODE_STILL_ASSOCIATED;
+ case MAX_REGISTERED_NODES_EXCEEDED -> MAX_REGISTERED_NODES_EXCEEDED;
case UNRECOGNIZED ->
// NOTE: Protobuf deserialization will not give us the code on the wire
throw new IllegalArgumentException(
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/Transaction.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/Transaction.java
index e09d63843..bcdca0098 100644
--- a/sdk/src/main/java/com/hedera/hashgraph/sdk/Transaction.java
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/Transaction.java
@@ -439,6 +439,9 @@ private static Transaction> createTransactionFromDataCase(
case CRYPTODELETEALLOWANCE -> new AccountAllowanceDeleteTransaction(txs);
case ATOMIC_BATCH -> new BatchTransaction(txs);
case HOOK_STORE -> new HookStoreTransaction(txs);
+ case REGISTEREDNODECREATE -> new RegisteredNodeCreateTransaction(txs);
+ case REGISTEREDNODEUPDATE -> new RegisteredNodeUpdateTransaction(txs);
+ case REGISTEREDNODEDELETE -> new RegisteredNodeDeleteTransaction(txs);
default -> throw new IllegalArgumentException("parsed transaction body has no data");
};
}
@@ -594,6 +597,15 @@ public static Transaction> fromScheduledTransaction(
case SCHEDULEDELETE ->
new ScheduleDeleteTransaction(
body.setScheduleDelete(scheduled.getScheduleDelete()).build());
+ case REGISTEREDNODECREATE ->
+ new RegisteredNodeCreateTransaction(body.setRegisteredNodeCreate(scheduled.getRegisteredNodeCreate())
+ .build());
+ case REGISTEREDNODEUPDATE ->
+ new RegisteredNodeUpdateTransaction(body.setRegisteredNodeUpdate(scheduled.getRegisteredNodeUpdate())
+ .build());
+ case REGISTEREDNODEDELETE ->
+ new RegisteredNodeDeleteTransaction(body.setRegisteredNodeDelete(scheduled.getRegisteredNodeDelete())
+ .build());
default -> throw new IllegalStateException("schedulable transaction did not have a transaction set");
};
}
diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/TransactionReceipt.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/TransactionReceipt.java
index 016ed95ab..6a7ee0d28 100644
--- a/sdk/src/main/java/com/hedera/hashgraph/sdk/TransactionReceipt.java
+++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/TransactionReceipt.java
@@ -118,6 +118,16 @@ public final class TransactionReceipt {
*/
public final long nodeId;
+ /**
+ * The identifier of a newly created RegisteredNode.
+ *
+ * This value SHALL be set following a `createRegisteredNode`
+ * transaction.
+ * This value SHALL NOT be set following any other transaction.
+ * This value SHALL be unique within a given network.
+ */
+ public final long registeredNodeId;
+
/**
* The receipts of processing all transactions with the given id, in consensus time order.
*/
@@ -146,6 +156,7 @@ public final class TransactionReceipt {
@Nullable TransactionId scheduledTransactionId,
List serials,
long nodeId,
+ long registeredNodeId,
List duplicates,
List children) {
this.transactionId = transactionId;
@@ -164,6 +175,7 @@ public final class TransactionReceipt {
this.scheduledTransactionId = scheduledTransactionId;
this.serials = serials;
this.nodeId = nodeId;
+ this.registeredNodeId = registeredNodeId;
this.duplicates = duplicates;
this.children = children;
}
@@ -218,6 +230,8 @@ static TransactionReceipt fromProtobuf(
var nodeId = transactionReceipt.getNodeId();
+ var registeredNodeId = transactionReceipt.getRegisteredNodeId();
+
return new TransactionReceipt(
transactionId,
status,
@@ -235,6 +249,7 @@ static TransactionReceipt fromProtobuf(
scheduledTransactionId,
serials,
nodeId,
+ registeredNodeId,
duplicates,
children);
}
@@ -345,6 +360,8 @@ com.hedera.hashgraph.sdk.proto.TransactionReceipt toProtobuf() {
transactionReceiptBuilder.setNodeId(nodeId);
+ transactionReceiptBuilder.setRegisteredNodeId(registeredNodeId);
+
return transactionReceiptBuilder.build();
}
@@ -367,6 +384,7 @@ public String toString() {
.add("scheduledTransactionId", scheduledTransactionId)
.add("serials", serials)
.add("nodeId", nodeId)
+ .add("registeredNodeId", registeredNodeId)
.add("duplicates", duplicates)
.add("children", children)
.toString();
diff --git a/sdk/src/main/proto/basic_types.proto b/sdk/src/main/proto/basic_types.proto
index c2b8b7be3..c43a9a149 100644
--- a/sdk/src/main/proto/basic_types.proto
+++ b/sdk/src/main/proto/basic_types.proto
@@ -1840,6 +1840,7 @@ enum HederaFunctionality {
AtomicBatch = 108;
/**
+ * Update one or more storage slots in an lambda EVM hook.
* (DEPRECATED) Remove once no production throttle assets reference it.
*/
LambdaSStore = 109;
diff --git a/sdk/src/main/proto/block_info.proto b/sdk/src/main/proto/block_info.proto
index 31b911c08..782965a10 100644
--- a/sdk/src/main/proto/block_info.proto
+++ b/sdk/src/main/proto/block_info.proto
@@ -109,4 +109,24 @@ message BlockInfo {
* at which an interval of time-dependent events were processed.
*/
proto.Timestamp last_interval_process_time = 8;
+
+ /**
+ * The root hash of the previous wrapped record block.
+ */
+ bytes previous_wrapped_record_block_root_hash = 10;
+
+ /**
+ * The intermediate hashes, calculated for all historical wrapped
+ * record blocks, needed for subroot 2 in the block merkle
+ * tree structure. These hashes SHALL include the minimum required
+ * wrapped record block root hashes needed to construct subroot 2's
+ * final state at the end of the previous block.
+ */
+ repeated bytes wrapped_intermediate_previous_block_root_hashes = 11;
+
+ /**
+ * The number of leaves in the intermediate wrapped record block
+ * roots subtree.
+ */
+ uint64 wrapped_intermediate_block_roots_leaf_count = 12;
}
diff --git a/sdk/src/main/proto/node_update.proto b/sdk/src/main/proto/node_update.proto
index 738ce9942..fe77d59cc 100644
--- a/sdk/src/main/proto/node_update.proto
+++ b/sdk/src/main/proto/node_update.proto
@@ -168,6 +168,7 @@ message NodeUpdateTransactionBody {
*/
proto.ServiceEndpoint grpc_proxy_endpoint = 10;
+
/**
* A list of registered nodes operated by the same entity as this node.
* This value may contain a list of "registered nodes" (as described in
diff --git a/sdk/src/main/proto/registered_service_endpoint.proto b/sdk/src/main/proto/registered_service_endpoint.proto
index 6bdbabf36..4ff655862 100644
--- a/sdk/src/main/proto/registered_service_endpoint.proto
+++ b/sdk/src/main/proto/registered_service_endpoint.proto
@@ -17,144 +17,165 @@ option java_multiple_files = true;
* or MAY include a FQDN _instead of_ an IP address.
*/
message RegisteredServiceEndpoint {
- /**
- * An IP address or fully qualified domain name.
- *
- * This oneof is REQUIRED.
- */
- oneof address {
/**
- * A 32-bit IPv4 address or 128-bit IPv6 address.
- * This is the address of the endpoint, encoded in pure "big-endian"
- * (i.e. left to right) order (e.g. IPv4 address `127.0.0.1` has
- * hex bytes in the order `7F`, `00`, `00`, `01`.
- * IPv6 address `::1` has hex bytes `00`, `00`, `00`, `00`, `00`, `00`,
- * `00`, `00`, `00`, `00`, `00`, `00`, `00`, `00`, `00`, `01`).
+ * An IP address or fully qualified domain name.
+ *
+ * This oneof is REQUIRED.
*/
- bytes ip_address = 1;
+ oneof address {
+ /**
+ * A 32-bit IPv4 address or 128-bit IPv6 address.
+ * This is the address of the endpoint, encoded in pure "big-endian"
+ * (i.e. left to right) order (e.g. IPv4 address `127.0.0.1` has
+ * hex bytes in the order `7F`, `00`, `00`, `01`.
+ * IPv6 address `::1` has hex bytes `00`, `00`, `00`, `00`, `00`, `00`,
+ * `00`, `00`, `00`, `00`, `00`, `00`, `00`, `00`, `00`, `01`).
+ */
+ bytes ip_address = 1;
+
+ /**
+ * A node domain name.
+ *
+ * This MUST be the fully qualified domain name of the node.
+ * This value MUST NOT exceed 250 ASCII characters.
+ * This value MUST meet all other DNS name requirements.
+ */
+ string domain_name = 2;
+ }
/**
- * A node domain name.
+ * A network port to use.
*
- * This MUST be the fully qualified domain name of the node.
- * This value MUST NOT exceed 250 ASCII characters.
- * This value MUST meet all other DNS name requirements.
+ * This value MUST be between 0 and 65535, inclusive.
+ * This value is REQUIRED.
*/
- string domain_name = 2;
- }
-
- /**
- * A network port to use.
- *
- * This value MUST be between 0 and 65535, inclusive.
- * This value is REQUIRED.
- */
- uint32 port = 3;
-
- /**
- * A flag indicating if this endpoint requires TLS.
- *
- * If this value is set true, then connections to this endpoint MUST
- * enable TLS.
- *
- * TLS endpoints MAY use self-signed certificates for this purpose,
- * but use of self-signed certificates SHOULD be limited to testing and
- * development environments to ensure production environments meet all
- * expected characteristics for transport layer security.
- */
- bool requires_tls = 4;
-
- /**
- * An endpoint type.
- * This declares the type of registered node endpoint and includes any
- * type-specific fields.
- *
- * This oneof is REQUIRED.
- */
- oneof endpoint_type {
+ uint32 port = 3;
+
/**
- * A Block Node.
- * A Block Node stores the block chain, provides content proof services,
- * and delivers the block stream to other clients.
+ * A flag indicating if this endpoint requires TLS.
+ *
+ * If this value is set true, then connections to this endpoint MUST
+ * enable TLS.
+ *
+ * TLS endpoints MAY use self-signed certificates for this purpose,
+ * but use of self-signed certificates SHOULD be limited to testing and
+ * development environments to ensure production environments meet all
+ * expected characteristics for transport layer security.
*/
- BlockNodeEndpoint block_node = 5;
+ bool requires_tls = 4;
/**
- * A Mirror Node.
- * A Mirror Node is an advanced indexing and query service that provides
- * fast and flexible access to query the block chain and transaction
- * history. A Mirror Node typically stores all recent blockchain data,
- * and some store the entire history of the network.
+ * An endpoint type.
+ * This declares the type of registered node endpoint and includes any
+ * type-specific fields.
+ *
+ * This oneof is REQUIRED.
*/
- MirrorNodeEndpoint mirror_node = 6;
+ oneof endpoint_type {
+ /**
+ * A Block Node.
+ * A Block Node stores the block chain, provides content proof services,
+ * and delivers the block stream to other clients.
+ */
+ BlockNodeEndpoint block_node = 5;
+
+ /**
+ * A Mirror Node.
+ * A Mirror Node is an advanced indexing and query service that provides
+ * fast and flexible access to query the block chain and transaction
+ * history. A Mirror Node typically stores all recent blockchain data,
+ * and some store the entire history of the network.
+ */
+ MirrorNodeEndpoint mirror_node = 6;
+
+ /**
+ * A RPC Relay.
+ * A RPC Relay is a proxy and translator between EVM tooling and a
+ * Hiero consensus network.
+ */
+ RpcRelayEndpoint rpc_relay = 7;
+
+ /**
+ * A general service.
+ * A general service endpoint represents any network accessible service
+ * that is provided by a registered node but that is not a service
+ * currently defined as part of the Hiero Ledger system.
+ */
+ GeneralServiceEndpoint general_service = 8;
+ }
/**
- * A RPC Relay.
- * A RPC Relay is a proxy and translator between EVM tooling and a
- * Hiero consensus network.
+ * A message indicating this endpoint is a Block Node endpoint.
+ * Block Node endpoints offer one of several block node APIs, so this
+ * endpoint entry also declares which well-known Block Node API is
+ * supported by this endpoint, or "OTHER" for less common APIs.
*/
- RpcRelayEndpoint rpc_relay = 7;
- }
-
- /**
- * A message indicating this endpoint is a Block Node endpoint.
- * Block Node endpoints offer one of several block node APIs, so this
- * endpoint entry also declares which well-known Block Node API is
- * supported by this endpoint, or "OTHER" for less common APIs.
- */
- message BlockNodeEndpoint {
+ message BlockNodeEndpoint {
+ /**
+ * An indicator of what API this endpoint supports.
+ *
+ * This field is REQUIRED.
+ */
+ repeated BlockNodeApi endpoint_api = 1;
+
+ /**
+ * An enumeration of well-known block node endpoint APIs, and a
+ * catch-all option for otherwise unknown endpoint APIs.
+ */
+ enum BlockNodeApi {
+ /**
+ * Any other API type associated with a block node.
+ * The caller must consult local documentation to determine the
+ * correct call semantics.
+ * It is RECOMMENDED to call the detail status endpoint for further
+ * information before using this endpoint.
+ */
+ OTHER = 0;
+
+ /**
+ * The Block Node Status API.
+ */
+ STATUS = 1;
+
+ /**
+ * The Block Node Publish API.
+ */
+ PUBLISH = 2;
+
+ /**
+ * The Block Node Subscribe Stream API.
+ */
+ SUBSCRIBE_STREAM = 3;
+
+ /**
+ * The Block Node State Proof API.
+ */
+ STATE_PROOF = 4;
+ }
+ }
+
/**
- * An indicator of what API this endpoint supports.
- *
- * This field is REQUIRED.
+ * A message indicating this endpoint is a Mirror Node endpoint.
*/
- BlockNodeApi endpoint_api = 1;
+ message MirrorNodeEndpoint {
+ }
+
+ /**
+ * A message indicating this endpoint is a RPC Relay endpoint.
+ */
+ message RpcRelayEndpoint {
+ }
/**
- * An enumeration of well-known block node endpoint APIs, and a
- * catch-all option for otherwise unknown endpoint APIs.
+ * A message indicating this endpoint is a General Service endpoint.
*/
- enum BlockNodeApi {
- /**
- * Any other API type associated with a block node.
- * The caller must consult local documentation to determine the
- * correct call semantics.
- * It is RECOMMENDED to call the detail status endpoint for further
- * information before using this endpoint.
- */
- OTHER = 0;
-
- /**
- * The Block Node Status API.
- */
- STATUS = 1;
-
- /**
- * The Block Node Publish API.
- */
- PUBLISH = 2;
-
- /**
- * The Block Node Subscribe Stream API.
- */
- SUBSCRIBE_STREAM = 3;
-
- /**
- * The Block Node State Proof API.
- */
- STATE_PROOF = 4;
+ message GeneralServiceEndpoint {
+ /**
+ * A short description of the service provided.
+ *
+ * This value, if set, MUST NOT exceed 100 bytes when encoded as UTF-8.
+ * This field is OPTIONAL.
+ */
+ string description = 1;
}
- }
-
- /**
- * A message indicating this endpoint is a Mirror Node endpoint.
- */
- message MirrorNodeEndpoint {
- }
-
- /**
- * A message indicating this endpoint is a RPC Relay endpoint.
- */
- message RpcRelayEndpoint {
- }
-}
\ No newline at end of file
+}
diff --git a/sdk/src/main/proto/response_code.proto b/sdk/src/main/proto/response_code.proto
index a48d14dd5..72a89742b 100644
--- a/sdk/src/main/proto/response_code.proto
+++ b/sdk/src/main/proto/response_code.proto
@@ -1935,4 +1935,44 @@ enum ResponseCodeEnum {
* The number of hook invocations exceeds the maximum allowed per transaction.
*/
TOO_MANY_HOOK_INVOCATIONS = 528;
+
+ /**
+ * A registered node ID is invalid or does not exist.
+ */
+ INVALID_REGISTERED_NODE_ID = 529;
+
+ /**
+ * A registered service endpoint is invalid.
+ * The port is out of range, or the address field is not set.
+ */
+ INVALID_REGISTERED_ENDPOINT = 530;
+
+ /**
+ * The number of registered service endpoints exceeds the configured limit.
+ */
+ REGISTERED_ENDPOINTS_EXCEEDED_LIMIT = 531;
+
+ /**
+ * A registered service endpoint has an invalid address.
+ * The IP address length is not 4 (IPv4) or 16 (IPv6), or the
+ * domain name is not a valid ASCII FQDN.
+ */
+ INVALID_REGISTERED_ENDPOINT_ADDRESS = 532;
+
+ /**
+ * A registered service endpoint does not specify an endpoint type.
+ * Exactly one of block_node, mirror_node, or rpc_relay MUST be set.
+ */
+ INVALID_REGISTERED_ENDPOINT_TYPE = 533;
+
+ /**
+ * A registered node cannot be deleted because it is still associated
+ * with a consensus node via their associated registered node list.
+ */
+ REGISTERED_NODE_STILL_ASSOCIATED = 534;
+
+ /**
+ * The number of associated registered nodes exceeds the maximum allowed limit.
+ */
+ MAX_REGISTERED_NODES_EXCEEDED = 535;
}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/BlockNodeApiTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/BlockNodeApiTest.java
new file mode 100644
index 000000000..cecff9f9b
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/BlockNodeApiTest.java
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+import com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint.BlockNodeEndpoint;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class BlockNodeApiTest {
+ @Test
+ @DisplayName("BlockNodeApi can be constructed for BlockNodeEndpoint API")
+ void blockNodeApiCodeToBlockNodeApi() {
+ for (BlockNodeEndpoint.BlockNodeApi code : BlockNodeEndpoint.BlockNodeApi.values()) {
+ if (code == BlockNodeEndpoint.BlockNodeApi.UNRECOGNIZED) {
+ continue;
+ }
+
+ BlockNodeApi blockNodeApi = BlockNodeApi.valueOf(code);
+ assertThat(code.getNumber()).isEqualTo(blockNodeApi.code.getNumber());
+ }
+ }
+
+ @Test
+ @DisplayName("BlockNodeApi throws on Unrecognized")
+ void blockNodeApiUnrecognized() {
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> BlockNodeApi.valueOf(BlockNodeEndpoint.BlockNodeApi.UNRECOGNIZED))
+ .withMessage("Unhandled BlockNodeApi code");
+ }
+}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/BlockNodeServiceEndpointTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/BlockNodeServiceEndpointTest.java
new file mode 100644
index 000000000..33318a960
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/BlockNodeServiceEndpointTest.java
@@ -0,0 +1,128 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import com.google.protobuf.ByteString;
+import com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint;
+import io.github.jsonSnapshot.SnapshotMatcher;
+import java.util.List;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class BlockNodeServiceEndpointTest {
+ private final byte[] TEST_IP_ADDRESS = new byte[] {1, 2, 3, 4};
+ private final String TEST_DOMAIN_NAME = "test.block.com";
+ private final int TEST_PORT = 443;
+ private final boolean TEST_REQUIRES_TLS = true;
+ private final List TEST_BLOCK_APIS = List.of(BlockNodeApi.STATUS);
+
+ private final RegisteredServiceEndpoint blockNodeEndpointWithDomain = RegisteredServiceEndpoint.newBuilder()
+ .setDomainName(TEST_DOMAIN_NAME)
+ .setPort(TEST_PORT)
+ .setRequiresTls(TEST_REQUIRES_TLS)
+ .setBlockNode(RegisteredServiceEndpoint.BlockNodeEndpoint.newBuilder()
+ .addAllEndpointApi(TEST_BLOCK_APIS.stream().map(e -> e.code).toList()))
+ .build();
+
+ private final RegisteredServiceEndpoint blockNodeEndpointWithIp = RegisteredServiceEndpoint.newBuilder()
+ .setIpAddress(ByteString.copyFrom(TEST_IP_ADDRESS))
+ .setPort(TEST_PORT)
+ .setRequiresTls(TEST_REQUIRES_TLS)
+ .setBlockNode(RegisteredServiceEndpoint.BlockNodeEndpoint.newBuilder()
+ .addAllEndpointApi(TEST_BLOCK_APIS.stream().map(e -> e.code).toList()))
+ .build();
+
+ @BeforeAll
+ public static void beforeAll() {
+ SnapshotMatcher.start(Snapshot::asJsonString);
+ }
+
+ @AfterAll
+ public static void afterAll() {
+ SnapshotMatcher.validateSnapshots();
+ }
+
+ @Test
+ void fromProtobufWithDomain() {
+ SnapshotMatcher.expect(BlockNodeServiceEndpoint.fromProtobuf(blockNodeEndpointWithDomain)
+ .toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void toProtobufWithDomain() {
+ SnapshotMatcher.expect(BlockNodeServiceEndpoint.fromProtobuf(blockNodeEndpointWithDomain)
+ .toProtobuf()
+ .toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void fromProtobufWithIp() {
+ SnapshotMatcher.expect(BlockNodeServiceEndpoint.fromProtobuf(blockNodeEndpointWithIp)
+ .toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void toProtobufWithIp() {
+ SnapshotMatcher.expect(BlockNodeServiceEndpoint.fromProtobuf(blockNodeEndpointWithIp)
+ .toProtobuf()
+ .toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void setIpAddress() {
+ var endpoint = new BlockNodeServiceEndpoint().setIpAddress(TEST_IP_ADDRESS);
+ assertThat(endpoint.getIpAddress()).isEqualTo(TEST_IP_ADDRESS);
+ }
+
+ @Test
+ void setDomainName() {
+ var endpoint = new BlockNodeServiceEndpoint().setDomainName(TEST_DOMAIN_NAME);
+ assertThat(endpoint.getDomainName()).isEqualTo(TEST_DOMAIN_NAME);
+ }
+
+ @Test
+ void setPort() {
+ var endpoint = new BlockNodeServiceEndpoint().setPort(TEST_PORT);
+ assertThat(endpoint.getPort()).isEqualTo(TEST_PORT);
+ }
+
+ @Test
+ void setPortThrowsOnNegative() {
+ var endpoint = new BlockNodeServiceEndpoint();
+ assertThatThrownBy(() -> endpoint.setPort(-1)).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void setPortThrowsOnGreaterThan65535() {
+ var endpoint = new BlockNodeServiceEndpoint();
+ assertThatThrownBy(() -> endpoint.setPort(65536)).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void setRequiresTls() {
+ var endpoint = new BlockNodeServiceEndpoint().setRequiresTls(TEST_REQUIRES_TLS);
+ assertThat(endpoint.isRequiresTls()).isEqualTo(TEST_REQUIRES_TLS);
+ }
+
+ @Test
+ void setEndpointApis() {
+ var endpoint = new BlockNodeServiceEndpoint().setEndpointApis(TEST_BLOCK_APIS);
+ assertThat(endpoint.getEndpointApis()).isEqualTo(TEST_BLOCK_APIS);
+ }
+
+ @Test
+ void addEndpointApi() {
+ var endpoint = new BlockNodeServiceEndpoint()
+ .addEndpointApi(BlockNodeApi.STATUS)
+ .addEndpointApi(BlockNodeApi.OTHER);
+
+ assertThat(endpoint.getEndpointApis()).containsExactly(BlockNodeApi.STATUS, BlockNodeApi.OTHER);
+ }
+}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/BlockNodeServiceEndpointTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/BlockNodeServiceEndpointTest.snap
new file mode 100644
index 000000000..b91a3674c
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/BlockNodeServiceEndpointTest.snap
@@ -0,0 +1,18 @@
+com.hedera.hashgraph.sdk.BlockNodeServiceEndpointTest.fromProtobufWithDomain=[
+ "BlockNodeServiceEndpoint{ipAddress=null, domainName=test.block.com, port=443, requiresTls=true, endpointApis=[STATUS]}"
+]
+
+
+com.hedera.hashgraph.sdk.BlockNodeServiceEndpointTest.fromProtobufWithIp=[
+ "BlockNodeServiceEndpoint{ipAddress=[1, 2, 3, 4], domainName=null, port=443, requiresTls=true, endpointApis=[STATUS]}"
+]
+
+
+com.hedera.hashgraph.sdk.BlockNodeServiceEndpointTest.toProtobufWithDomain=[
+ "# com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint@855debe4\nblock_node {\n endpoint_api: STATUS\n endpoint_api_value: 1\n}\ndomain_name: \"test.block.com\"\nport: 443\nrequires_tls: true"
+]
+
+
+com.hedera.hashgraph.sdk.BlockNodeServiceEndpointTest.toProtobufWithIp=[
+ "# com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint@86fe846\nblock_node {\n endpoint_api: STATUS\n endpoint_api_value: 1\n}\nip_address: \"\\001\\002\\003\\004\"\nport: 443\nrequires_tls: true"
+]
\ No newline at end of file
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/GeneralServiceEndpointTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/GeneralServiceEndpointTest.java
new file mode 100644
index 000000000..76f18aa02
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/GeneralServiceEndpointTest.java
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.google.protobuf.ByteString;
+import com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint;
+import io.github.jsonSnapshot.SnapshotMatcher;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class GeneralServiceEndpointTest {
+ private static final byte[] TEST_IP_ADDRESS = new byte[] {1, 2, 3, 4};
+ private static final String TEST_DOMAIN_NAME = "general.service.com";
+ private static final String TEST_DESCRIPTION = "A general purpose endpoint.";
+ private static final int TEST_PORT = 8080;
+ private static final boolean TEST_REQUIRES_TLS = false;
+
+ private final RegisteredServiceEndpoint generalEndpointWithDomain = RegisteredServiceEndpoint.newBuilder()
+ .setDomainName(TEST_DOMAIN_NAME)
+ .setPort(TEST_PORT)
+ .setRequiresTls(TEST_REQUIRES_TLS)
+ .setGeneralService(RegisteredServiceEndpoint.GeneralServiceEndpoint.newBuilder()
+ .setDescription(TEST_DESCRIPTION))
+ .build();
+
+ private final RegisteredServiceEndpoint generalEndpointWithIp = RegisteredServiceEndpoint.newBuilder()
+ .setIpAddress(ByteString.copyFrom(TEST_IP_ADDRESS))
+ .setPort(TEST_PORT)
+ .setRequiresTls(TEST_REQUIRES_TLS)
+ .setGeneralService(RegisteredServiceEndpoint.GeneralServiceEndpoint.newBuilder()
+ .setDescription(TEST_DESCRIPTION))
+ .build();
+
+ @BeforeAll
+ public static void beforeAll() {
+ SnapshotMatcher.start(Snapshot::asJsonString);
+ }
+
+ @AfterAll
+ public static void afterAll() {
+ SnapshotMatcher.validateSnapshots();
+ }
+
+ @Test
+ void fromProtobufWithIp() {
+ SnapshotMatcher.expect(GeneralServiceEndpoint.fromProtobuf(generalEndpointWithIp)
+ .toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void fromProtobufWithDomain() {
+ SnapshotMatcher.expect(GeneralServiceEndpoint.fromProtobuf(generalEndpointWithDomain)
+ .toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void setDescription() {
+ var endpoint = new GeneralServiceEndpoint().setDescription(TEST_DESCRIPTION);
+ assertThat(endpoint.getDescription()).isEqualTo(TEST_DESCRIPTION);
+ }
+}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/GeneralServiceEndpointTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/GeneralServiceEndpointTest.snap
new file mode 100644
index 000000000..70c25ee9c
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/GeneralServiceEndpointTest.snap
@@ -0,0 +1,8 @@
+com.hedera.hashgraph.sdk.GeneralServiceEndpointTest.fromProtobufWithDomain=[
+ "GeneralServiceEndpoint{ipAddress=null, domainName=general.service.com, port=8080, requiresTls=false, description=A general purpose endpoint.}"
+]
+
+
+com.hedera.hashgraph.sdk.GeneralServiceEndpointTest.fromProtobufWithIp=[
+ "GeneralServiceEndpoint{ipAddress=[1, 2, 3, 4], domainName=null, port=8080, requiresTls=false, description=A general purpose endpoint.}"
+]
\ No newline at end of file
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/MirrorNodeServiceEndpointTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/MirrorNodeServiceEndpointTest.java
new file mode 100644
index 000000000..d4c0996f7
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/MirrorNodeServiceEndpointTest.java
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import com.google.protobuf.ByteString;
+import com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint;
+import io.github.jsonSnapshot.SnapshotMatcher;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class MirrorNodeServiceEndpointTest {
+ private final byte[] TEST_IP_ADDRESS = new byte[] {1, 2, 3, 4};
+ private final String TEST_DOMAIN_NAME = "test.mirror.com";
+ private final int TEST_PORT = 443;
+ private final boolean TEST_REQUIRES_TLS = true;
+
+ private final com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint mirrorNodeEndpointWithDomain =
+ com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint.newBuilder()
+ .setDomainName(TEST_DOMAIN_NAME)
+ .setPort(TEST_PORT)
+ .setRequiresTls(TEST_REQUIRES_TLS)
+ .setMirrorNode(RegisteredServiceEndpoint.MirrorNodeEndpoint.newBuilder())
+ .build();
+
+ private final com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint mirrorNodeEndpointWithIp =
+ com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint.newBuilder()
+ .setIpAddress(ByteString.copyFrom(TEST_IP_ADDRESS))
+ .setPort(TEST_PORT)
+ .setRequiresTls(TEST_REQUIRES_TLS)
+ .setMirrorNode(RegisteredServiceEndpoint.MirrorNodeEndpoint.newBuilder())
+ .build();
+
+ @BeforeAll
+ public static void beforeAll() {
+ SnapshotMatcher.start(Snapshot::asJsonString);
+ }
+
+ @AfterAll
+ public static void afterAll() {
+ SnapshotMatcher.validateSnapshots();
+ }
+
+ @Test
+ void fromProtobufWithDomain() {
+ SnapshotMatcher.expect(MirrorNodeServiceEndpoint.fromProtobuf(mirrorNodeEndpointWithDomain)
+ .toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void toProtobufWithDomain() {
+ SnapshotMatcher.expect(MirrorNodeServiceEndpoint.fromProtobuf(mirrorNodeEndpointWithDomain)
+ .toProtobuf()
+ .toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void fromProtobufWithIp() {
+ SnapshotMatcher.expect(MirrorNodeServiceEndpoint.fromProtobuf(mirrorNodeEndpointWithIp)
+ .toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void toProtobufWithIp() {
+ SnapshotMatcher.expect(MirrorNodeServiceEndpoint.fromProtobuf(mirrorNodeEndpointWithIp)
+ .toProtobuf()
+ .toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void setIpAddress() {
+ var endpoint = new BlockNodeServiceEndpoint().setIpAddress(TEST_IP_ADDRESS);
+ assertThat(endpoint.getIpAddress()).isEqualTo(TEST_IP_ADDRESS);
+ }
+
+ @Test
+ void setDomainName() {
+ var endpoint = new BlockNodeServiceEndpoint().setDomainName(TEST_DOMAIN_NAME);
+ assertThat(endpoint.getDomainName()).isEqualTo(TEST_DOMAIN_NAME);
+ }
+
+ @Test
+ void setPort() {
+ var endpoint = new BlockNodeServiceEndpoint().setPort(TEST_PORT);
+ assertThat(endpoint.getPort()).isEqualTo(TEST_PORT);
+ }
+
+ @Test
+ void setPortThrowsOnNegative() {
+ var endpoint = new BlockNodeServiceEndpoint();
+ assertThatThrownBy(() -> endpoint.setPort(-1)).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void setPortThrowsOnGreaterThan65535() {
+ var endpoint = new BlockNodeServiceEndpoint();
+ assertThatThrownBy(() -> endpoint.setPort(65536)).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void setRequiresTls() {
+ var endpoint = new BlockNodeServiceEndpoint().setRequiresTls(TEST_REQUIRES_TLS);
+ assertThat(endpoint.isRequiresTls()).isEqualTo(TEST_REQUIRES_TLS);
+ }
+}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/MirrorNodeServiceEndpointTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/MirrorNodeServiceEndpointTest.snap
new file mode 100644
index 000000000..5d2379d62
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/MirrorNodeServiceEndpointTest.snap
@@ -0,0 +1,18 @@
+com.hedera.hashgraph.sdk.MirrorNodeServiceEndpointTest.fromProtobufWithDomain=[
+ "MirrorNodeServiceEndpoint{ipAddress=null, domainName=test.mirror.com, port=443, requiresTls=true}"
+]
+
+
+com.hedera.hashgraph.sdk.MirrorNodeServiceEndpointTest.fromProtobufWithIp=[
+ "MirrorNodeServiceEndpoint{ipAddress=[1, 2, 3, 4], domainName=null, port=443, requiresTls=true}"
+]
+
+
+com.hedera.hashgraph.sdk.MirrorNodeServiceEndpointTest.toProtobufWithDomain=[
+ "# com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint@28438c0e\ndomain_name: \"test.mirror.com\"\nmirror_node {\n}\nport: 443\nrequires_tls: true"
+]
+
+
+com.hedera.hashgraph.sdk.MirrorNodeServiceEndpointTest.toProtobufWithIp=[
+ "# com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint@86e8926\nip_address: \"\\001\\002\\003\\004\"\nmirror_node {\n}\nport: 443\nrequires_tls: true"
+]
\ No newline at end of file
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.java
index 73d41dc92..8d16dc112 100644
--- a/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.java
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.java
@@ -40,6 +40,8 @@ public class NodeCreateTransactionTest {
private static final byte[] TEST_GRPC_CERTIFICATE_HASH = new byte[] {5, 6, 7, 8, 9};
+ private static final List TEST_ASSOCIATED_REGISTERED_NODES = List.of(1L, 2L);
+
private static final PublicKey TEST_ADMIN_KEY = PrivateKey.fromString(
"302e020100300506032b65700422042062c4b69e9f45a554e5424fb5a6fe5e6ac1f19ead31dc7718c2d980fd1f998d4b")
.getPublicKey();
@@ -80,6 +82,7 @@ private NodeCreateTransaction spawnTestTransaction() {
.setMaxTransactionFee(new Hbar(1))
.setDeclineReward(false) // accepts the reward
.setGrpcWebProxyEndpoint(TEST_GRPC_WEB_PROXY_ENDPOINT)
+ .setAssociatedRegisteredNodes(TEST_ASSOCIATED_REGISTERED_NODES)
.freeze()
.sign(TEST_PRIVATE_KEY);
}
@@ -415,4 +418,45 @@ void setGrpcWebProxyEndpointRequiresFrozen() {
var tx = spawnTestTransaction();
assertThrows(IllegalStateException.class, () -> tx.setGrpcWebProxyEndpoint(TEST_GRPC_WEB_PROXY_ENDPOINT));
}
+
+ @Test
+ void setAssociatedRegisteredNodes() {
+ var tx = new NodeCreateTransaction().setAssociatedRegisteredNodes(TEST_ASSOCIATED_REGISTERED_NODES);
+ assertThat(tx.getAssociatedRegisteredNodes()).isEqualTo(TEST_ASSOCIATED_REGISTERED_NODES);
+ }
+
+ @Test
+ void setAssociatedRegisteredNodesFrozen() {
+ var tx = spawnTestTransaction();
+ assertThrows(
+ IllegalStateException.class, () -> tx.setAssociatedRegisteredNodes(TEST_ASSOCIATED_REGISTERED_NODES));
+ }
+
+ @Test
+ void setAssociatedRegisteredNodesMoreThan20() {
+ var tx = new NodeCreateTransaction();
+ var nodes = new java.util.ArrayList();
+ for (int i = 0; i < 21; i++) {
+ nodes.add((long) i);
+ }
+
+ assertThrows(IllegalArgumentException.class, () -> tx.setAssociatedRegisteredNodes(nodes));
+ }
+
+ @Test
+ void addAssociatedRegisteredNode() {
+ var tx = new NodeCreateTransaction();
+ tx.addAssociatedRegisteredNode(1L);
+ assertThat(tx.getAssociatedRegisteredNodes()).hasSize(1);
+ assertThat(tx.getAssociatedRegisteredNodes().get(0)).isEqualTo(1);
+ }
+
+ @Test
+ void addAssociatedRegisteredNodeMoreThan20() {
+ var tx = new NodeCreateTransaction();
+ for (int i = 0; i < 20; i++) {
+ tx.addAssociatedRegisteredNode((long) i);
+ }
+ assertThrows(IllegalArgumentException.class, () -> tx.addAssociatedRegisteredNode(21L));
+ }
}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.snap
index ab38ebfcf..3cc56dcbd 100644
--- a/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.snap
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.snap
@@ -1,3 +1,3 @@
com.hedera.hashgraph.sdk.NodeCreateTransactionTest.shouldSerialize=[
- "# com.hedera.hashgraph.sdk.proto.TransactionBody\nnode_account_i_d {\n account_num: 5005\n realm_num: 0\n shard_num: 0\n}\nnode_create {\n account_id {\n account_num: 9\n realm_num: 6\n shard_num: 0\n }\n admin_key {\n ed25519: \"\\030\\214\\252`\\231\\024#O\\b\\370\\342\\232\\321g\\274\\273\\346\\221}\\211m\\244R\\306\\017\\230\\017j,\\246\\206\\001\"\n }\n description: \"Test description\"\n gossip_ca_certificate: \"\\000\\001\\002\\003\\004\"\n gossip_endpoint {\n domain_name: \"0unit.test.com\"\n port: 42\n }\n gossip_endpoint {\n domain_name: \"1unit.test.com\"\n port: 43\n }\n gossip_endpoint {\n domain_name: \"2unit.test.com\"\n port: 44\n }\n grpc_certificate_hash: \"\\005\\006\\a\\b\\t\"\n grpc_proxy_endpoint {\n domain_name: \"3unit.test.com\"\n port: 45\n }\n service_endpoint {\n domain_name: \"3unit.test.com\"\n port: 45\n }\n service_endpoint {\n domain_name: \"4unit.test.com\"\n port: 46\n }\n service_endpoint {\n domain_name: \"5unit.test.com\"\n port: 47\n }\n service_endpoint {\n domain_name: \"6unit.test.com\"\n port: 48\n }\n}\ntransaction_fee: 100000000\ntransaction_i_d {\n account_i_d {\n account_num: 5006\n realm_num: 0\n shard_num: 0\n }\n transaction_valid_start {\n seconds: 1554158542\n }\n}\ntransaction_valid_duration {\n seconds: 120\n}"
+ "# com.hedera.hashgraph.sdk.proto.TransactionBody\nnode_account_i_d {\n account_num: 5005\n realm_num: 0\n shard_num: 0\n}\nnode_create {\n account_id {\n account_num: 9\n realm_num: 6\n shard_num: 0\n }\n admin_key {\n ed25519: \"\\030\\214\\252`\\231\\024#O\\b\\370\\342\\232\\321g\\274\\273\\346\\221}\\211m\\244R\\306\\017\\230\\017j,\\246\\206\\001\"\n }\n associated_registered_node: 1\n associated_registered_node: 2\n description: \"Test description\"\n gossip_ca_certificate: \"\\000\\001\\002\\003\\004\"\n gossip_endpoint {\n domain_name: \"0unit.test.com\"\n port: 42\n }\n gossip_endpoint {\n domain_name: \"1unit.test.com\"\n port: 43\n }\n gossip_endpoint {\n domain_name: \"2unit.test.com\"\n port: 44\n }\n grpc_certificate_hash: \"\\005\\006\\a\\b\\t\"\n grpc_proxy_endpoint {\n domain_name: \"3unit.test.com\"\n port: 45\n }\n service_endpoint {\n domain_name: \"3unit.test.com\"\n port: 45\n }\n service_endpoint {\n domain_name: \"4unit.test.com\"\n port: 46\n }\n service_endpoint {\n domain_name: \"5unit.test.com\"\n port: 47\n }\n service_endpoint {\n domain_name: \"6unit.test.com\"\n port: 48\n }\n}\ntransaction_fee: 100000000\ntransaction_i_d {\n account_i_d {\n account_num: 5006\n realm_num: 0\n shard_num: 0\n }\n transaction_valid_start {\n seconds: 1554158542\n }\n}\ntransaction_valid_duration {\n seconds: 120\n}"
]
\ No newline at end of file
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.java
index 1499c5072..d5546d248 100644
--- a/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.java
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.java
@@ -47,6 +47,8 @@ public class NodeUpdateTransactionTest {
private static final byte[] TEST_GRPC_CERTIFICATE_HASH = new byte[48]; // SHA-384 hash (48 bytes)
+ private static final List TEST_ASSOCIATED_REGISTERED_NODES = List.of(1L, 2L);
+
private static final PublicKey TEST_ADMIN_KEY = PrivateKey.fromString(
"302e020100300506032b65700422042062c4b69e9f45a554e5424fb5a6fe5e6ac1f19ead31dc7718c2d980fd1f998d4b")
.getPublicKey();
@@ -91,6 +93,7 @@ private NodeUpdateTransaction spawnTestTransaction() {
.setMaxTransactionFee(new Hbar(1))
.setDeclineReward(true)
.setGrpcWebProxyEndpoint(TEST_GRPC_WEB_PROXY_ENDPOINT)
+ .setAssociatedRegisteredNodes(TEST_ASSOCIATED_REGISTERED_NODES)
.freeze()
.sign(TEST_PRIVATE_KEY);
}
@@ -618,4 +621,45 @@ void shouldAllowEmptyGrpcCertificateHash() {
// Empty is allowed because network will validate it
assertThatCode(() -> transaction.setGrpcCertificateHash(new byte[] {})).doesNotThrowAnyException();
}
+
+ @Test
+ void setAssociatedRegisteredNodes() {
+ var tx = new NodeCreateTransaction().setAssociatedRegisteredNodes(TEST_ASSOCIATED_REGISTERED_NODES);
+ assertThat(tx.getAssociatedRegisteredNodes()).isEqualTo(TEST_ASSOCIATED_REGISTERED_NODES);
+ }
+
+ @Test
+ void setAssociatedRegisteredNodesFrozen() {
+ var tx = spawnTestTransaction();
+ assertThrows(
+ IllegalStateException.class, () -> tx.setAssociatedRegisteredNodes(TEST_ASSOCIATED_REGISTERED_NODES));
+ }
+
+ @Test
+ void setAssociatedRegisteredNodesMoreThan20() {
+ var tx = new NodeCreateTransaction();
+ var nodes = new java.util.ArrayList();
+ for (int i = 0; i < 21; i++) {
+ nodes.add((long) i);
+ }
+
+ assertThrows(IllegalArgumentException.class, () -> tx.setAssociatedRegisteredNodes(nodes));
+ }
+
+ @Test
+ void addAssociatedRegisteredNode() {
+ var tx = new NodeCreateTransaction();
+ tx.addAssociatedRegisteredNode(1L);
+ assertThat(tx.getAssociatedRegisteredNodes()).hasSize(1);
+ assertThat(tx.getAssociatedRegisteredNodes().get(0)).isEqualTo(1);
+ }
+
+ @Test
+ void addAssociatedRegisteredNodeMoreThan20() {
+ var tx = new NodeCreateTransaction();
+ for (int i = 0; i < 20; i++) {
+ tx.addAssociatedRegisteredNode((long) i);
+ }
+ assertThrows(IllegalArgumentException.class, () -> tx.addAssociatedRegisteredNode(21L));
+ }
}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.snap
index df0510558..6cd7b6e4d 100644
--- a/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.snap
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.snap
@@ -1,3 +1,3 @@
com.hedera.hashgraph.sdk.NodeUpdateTransactionTest.shouldSerialize=[
- "# com.hedera.hashgraph.sdk.proto.TransactionBody\nnode_account_i_d {\n account_num: 5005\n realm_num: 0\n shard_num: 0\n}\nnode_update {\n account_id {\n account_num: 9\n realm_num: 6\n shard_num: 0\n }\n admin_key {\n ed25519: \"\\030\\214\\252`\\231\\024#O\\b\\370\\342\\232\\321g\\274\\273\\346\\221}\\211m\\244R\\306\\017\\230\\017j,\\246\\206\\001\"\n }\n decline_reward {\n value: true\n }\n description {\n value: \"Test description\"\n }\n gossip_ca_certificate {\n value: \"\\000\\001\\002\\003\\004\"\n }\n gossip_endpoint {\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 42\n }\n gossip_endpoint {\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 43\n }\n gossip_endpoint {\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 44\n }\n grpc_certificate_hash {\n value: \"\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\"\n }\n grpc_proxy_endpoint {\n domain_name: \"3unit.test.com\"\n port: 45\n }\n node_id: 420\n service_endpoint {\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 45\n }\n service_endpoint {\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 46\n }\n service_endpoint {\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 47\n }\n service_endpoint {\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 48\n }\n}\ntransaction_fee: 100000000\ntransaction_i_d {\n account_i_d {\n account_num: 5006\n realm_num: 0\n shard_num: 0\n }\n transaction_valid_start {\n seconds: 1554158542\n }\n}\ntransaction_valid_duration {\n seconds: 120\n}"
-]
+ "# com.hedera.hashgraph.sdk.proto.TransactionBody\nnode_account_i_d {\n account_num: 5005\n realm_num: 0\n shard_num: 0\n}\nnode_update {\n account_id {\n account_num: 9\n realm_num: 6\n shard_num: 0\n }\n admin_key {\n ed25519: \"\\030\\214\\252`\\231\\024#O\\b\\370\\342\\232\\321g\\274\\273\\346\\221}\\211m\\244R\\306\\017\\230\\017j,\\246\\206\\001\"\n }\n associated_registered_node_list {\n associated_registered_node: 1\n associated_registered_node: 2\n }\n decline_reward {\n value: true\n }\n description {\n value: \"Test description\"\n }\n gossip_ca_certificate {\n value: \"\\000\\001\\002\\003\\004\"\n }\n gossip_endpoint {\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 42\n }\n gossip_endpoint {\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 43\n }\n gossip_endpoint {\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 44\n }\n grpc_certificate_hash {\n value: \"\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\"\n }\n grpc_proxy_endpoint {\n domain_name: \"3unit.test.com\"\n port: 45\n }\n node_id: 420\n service_endpoint {\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 45\n }\n service_endpoint {\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 46\n }\n service_endpoint {\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 47\n }\n service_endpoint {\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 48\n }\n}\ntransaction_fee: 100000000\ntransaction_i_d {\n account_i_d {\n account_num: 5006\n realm_num: 0\n shard_num: 0\n }\n transaction_valid_start {\n seconds: 1554158542\n }\n}\ntransaction_valid_duration {\n seconds: 120\n}"
+]
\ No newline at end of file
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeAddressBookQueryTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeAddressBookQueryTest.java
new file mode 100644
index 000000000..7d32c10fe
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeAddressBookQueryTest.java
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class RegisteredNodeAddressBookQueryTest {
+ private RegisteredNodeAddressBookQuery query;
+ private Client mockClient;
+
+ @BeforeEach
+ void setUp() {
+ query = Mockito.spy(new RegisteredNodeAddressBookQuery());
+ mockClient = Mockito.mock(Client.class);
+ Mockito.when(mockClient.getMirrorRestBaseUrl()).thenReturn("https://testnet.mirrornode.hedera.com");
+ }
+
+ @Test
+ void testExecuteSuccess() throws ExecutionException, InterruptedException {
+ var mockResponse = "{" + " \"registered_nodes\": ["
+ + " {"
+ + " \"registered_node_id\": 123,"
+ + " \"description\": \"Test Node\","
+ + " \"admin_key\": {"
+ + " \"_type\": \"ED25519\","
+ + " \"key\": \"302a300506032b6570032100e0c8ec27b039a7d094a6132049386d9a0d8e8751508241473919e1c455f75605\""
+ + " },"
+ + " \"service_endpoints\": ["
+ + " {"
+ + " \"type\": \"BLOCK_NODE\","
+ + " \"port\": 50211,"
+ + " \"requires_tls\": true,"
+ + " \"ip_address\": \"127.0.0.1\","
+ + " \"block_node\": { \"endpoint_apis\": [\"OTHER\"] }"
+ + " }"
+ + " ]"
+ + " }"
+ + " ]"
+ + "}";
+
+ Mockito.doReturn(CompletableFuture.completedFuture(mockResponse))
+ .when(query)
+ .executeMirrorNodeRequest(Mockito.any(Client.class));
+
+ query.setRegisteredNodeId(123L);
+ var result = query.execute(mockClient);
+
+ Assertions.assertThat(result).isNotNull();
+ Assertions.assertThat(result.registeredNodes).hasSize(1);
+
+ var node = result.registeredNodes.get(0);
+ Assertions.assertThat(node.registeredNodeId).isEqualTo(123L);
+ Assertions.assertThat(node.description).isEqualTo("Test Node");
+ Assertions.assertThat(node.adminKey).isNotNull();
+ Assertions.assertThat(node.adminKey instanceof PublicKey).isTrue();
+
+ var endpoint = node.serviceEndpoints.get(0);
+ Assertions.assertThat(endpoint instanceof BlockNodeServiceEndpoint).isTrue();
+ Assertions.assertThat(endpoint.getPort()).isEqualTo(50211);
+ Assertions.assertThat(endpoint.isRequiresTls()).isTrue();
+ }
+}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeCreateTransactionTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeCreateTransactionTest.java
new file mode 100644
index 000000000..b0193e751
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeCreateTransactionTest.java
@@ -0,0 +1,230 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.hedera.hashgraph.sdk.proto.RegisteredNodeCreateTransactionBody;
+import com.hedera.hashgraph.sdk.proto.SchedulableTransactionBody;
+import com.hedera.hashgraph.sdk.proto.TransactionBody;
+import io.github.jsonSnapshot.SnapshotMatcher;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class RegisteredNodeCreateTransactionTest {
+ private static final PrivateKey TEST_PRIVATE_KEY = PrivateKey.fromString(
+ "302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c97732394482538e10");
+
+ private static final AccountId TEST_ACCOUNT_ID = AccountId.fromString("0.6.9");
+
+ private static final String TEST_DESCRIPTION = "Test description";
+
+ private static final List TEST_SERVICE_ENDPOINT =
+ List.of(spawnTestEndpoint((byte) 0), spawnTestEndpoint((byte) 1), spawnTestEndpoint((byte) 2));
+
+ private static final PublicKey TEST_ADMIN_KEY = PrivateKey.fromString(
+ "302e020100300506032b65700422042062c4b69e9f45a554e5424fb5a6fe5e6ac1f19ead31dc7718c2d980fd1f998d4b")
+ .getPublicKey();
+
+ final Instant TEST_VALID_START = Instant.ofEpochSecond(1554158542);
+
+ static RegisteredServiceEndpoint spawnTestEndpoint(byte offset) {
+ return new BlockNodeServiceEndpoint()
+ .setDomainName("example.block.com")
+ .setPort(443 + offset)
+ .setRequiresTls(true)
+ .addEndpointApi(BlockNodeApi.STATUS);
+ }
+
+ RegisteredNodeCreateTransaction spawnTestTransaction() {
+ return new RegisteredNodeCreateTransaction()
+ .setNodeAccountIds(Arrays.asList(AccountId.fromString("0.0.5005"), AccountId.fromString("0.0.5006")))
+ .setTransactionId(TransactionId.withValidStart(AccountId.fromString("0.0.5006"), TEST_VALID_START))
+ .setAdminKey(TEST_ADMIN_KEY)
+ .setDescription(TEST_DESCRIPTION)
+ .setServiceEndpoints(TEST_SERVICE_ENDPOINT)
+ .setMaxTransactionFee(new Hbar(1))
+ .freeze()
+ .sign(TEST_PRIVATE_KEY);
+ }
+
+ @BeforeAll
+ public static void beforeAll() {
+ SnapshotMatcher.start(Snapshot::asJsonString);
+ }
+
+ @AfterAll
+ public static void afterAll() {
+ SnapshotMatcher.validateSnapshots();
+ }
+
+ @Test
+ void shouldSerialize() {
+ SnapshotMatcher.expect(spawnTestTransaction().toString()).toMatchSnapshot();
+ }
+
+ @Test
+ void shouldBytes() throws Exception {
+ var tx1 = spawnTestTransaction();
+ var tx2 = RegisteredNodeCreateTransaction.fromBytes(tx1.toBytes());
+ assertThat(tx2.toString()).isEqualTo(tx1.toString());
+ }
+
+ @Test
+ void shouldBytesNoSetters() throws Exception {
+ var tx1 = new RegisteredNodeCreateTransaction();
+ var tx2 = Transaction.fromBytes(tx1.toBytes());
+ assertThat(tx2.toString()).isEqualTo(tx1.toString());
+ }
+
+ @Test
+ void fromScheduledTransaction() {
+ var transactionBody = SchedulableTransactionBody.newBuilder()
+ .setRegisteredNodeCreate(
+ RegisteredNodeCreateTransactionBody.newBuilder().build())
+ .build();
+
+ var tx = Transaction.fromScheduledTransaction(transactionBody);
+
+ assertThat(tx).isInstanceOf(RegisteredNodeCreateTransaction.class);
+ }
+
+ @Test
+ void setAdminKey() {
+ var tx = new RegisteredNodeCreateTransaction().setAdminKey(TEST_ADMIN_KEY);
+ assertThat(tx.getAdminKey()).isEqualTo(TEST_ADMIN_KEY);
+ }
+
+ @Test
+ void setAdminKeyFrozen() {
+ var tx = spawnTestTransaction();
+ assertThrows(IllegalStateException.class, () -> tx.setAdminKey(TEST_ADMIN_KEY));
+ }
+
+ @Test
+ void setDescription() {
+ var tx = new RegisteredNodeCreateTransaction().setDescription(TEST_DESCRIPTION);
+ assertThat(tx.getDescription()).isEqualTo(TEST_DESCRIPTION);
+ }
+
+ @Test
+ void setDescriptionFrozen() {
+ var tx = spawnTestTransaction();
+ assertThrows(IllegalStateException.class, () -> tx.setDescription(TEST_DESCRIPTION));
+ }
+
+ @Test
+ void setDescriptionRejectsOver100Utf8Bytes() {
+ var tx = new RegisteredNodeCreateTransaction();
+ String tooLong = "a".repeat(101);
+ assertThrows(IllegalArgumentException.class, () -> tx.setDescription(tooLong));
+ }
+
+ @Test
+ void setDescriptionAcceptsExactly100Utf8Bytes() {
+ var tx = new RegisteredNodeCreateTransaction();
+ String exact = "a".repeat(100);
+ tx.setDescription(exact);
+ assertThat(tx.getDescription()).isEqualTo(exact);
+ }
+
+ @Test
+ void setServiceEndpoint() {
+ var tx = new RegisteredNodeCreateTransaction().setServiceEndpoints(TEST_SERVICE_ENDPOINT);
+ assertThat(tx.getServiceEndpoints()).hasSize(TEST_SERVICE_ENDPOINT.size());
+ assertThat(tx.getServiceEndpoints()).isEqualTo(TEST_SERVICE_ENDPOINT);
+ }
+
+ @Test
+ void setServiceEndpointFrozen() {
+ var tx = spawnTestTransaction();
+ assertThrows(IllegalStateException.class, () -> tx.setServiceEndpoints(TEST_SERVICE_ENDPOINT));
+ }
+
+ @Test
+ void setServiceEndpointRejectsMoreThan50() {
+ var tx = new RegisteredNodeCreateTransaction();
+ var serviceEndpoints = new ArrayList();
+ for (int i = 0; i < 51; i++) {
+ serviceEndpoints.add(spawnTestEndpoint((byte) i));
+ }
+
+ assertThrows(IllegalArgumentException.class, () -> tx.setServiceEndpoints(serviceEndpoints));
+ }
+
+ @Test
+ void addServiceEndpoint() {
+ var tx = new RegisteredNodeCreateTransaction();
+ var serviceEndpoint = spawnTestEndpoint((byte) 1);
+
+ tx.addServiceEndpoint(serviceEndpoint);
+ assertThat(tx.getServiceEndpoints()).hasSize(1);
+ assertThat(tx.getServiceEndpoints().get(0)).isEqualTo(serviceEndpoint);
+ }
+
+ @Test
+ void addServiceEndpointRejectsMoreThan50() {
+ var tx = new RegisteredNodeCreateTransaction();
+ for (int i = 0; i < 50; i++) {
+ tx.addServiceEndpoint(spawnTestEndpoint((byte) i));
+ }
+
+ assertThrows(IllegalArgumentException.class, () -> tx.addServiceEndpoint(spawnTestEndpoint((byte) 50)));
+ }
+
+ @Test
+ void constructRegisteredNodeCreateTransactionFromTransactionBodyProtobuf() {
+ var transactionBodyBuilder = RegisteredNodeCreateTransactionBody.newBuilder();
+
+ transactionBodyBuilder.setAdminKey(TEST_ADMIN_KEY.toProtobufKey());
+ transactionBodyBuilder.setDescription(TEST_DESCRIPTION);
+
+ for (RegisteredServiceEndpoint serviceEndpoint : TEST_SERVICE_ENDPOINT) {
+ transactionBodyBuilder.addServiceEndpoint(serviceEndpoint.toProtobuf());
+ }
+
+ var transactionBody = TransactionBody.newBuilder()
+ .setRegisteredNodeCreate(transactionBodyBuilder.build())
+ .build();
+ var tx = new RegisteredNodeCreateTransaction(transactionBody);
+
+ assertThat(tx.getAdminKey()).isEqualTo(TEST_ADMIN_KEY);
+ assertThat(tx.getDescription()).isEqualTo(TEST_DESCRIPTION);
+ assertThat(tx.getServiceEndpoints()).hasSize(TEST_SERVICE_ENDPOINT.size());
+ }
+
+ @Test
+ void shouldFreezeSuccessfullyWhenAdminKeySet() {
+ final Instant VALID_START = Instant.ofEpochSecond(1596210382);
+ final AccountId ACCOUNT_ID = AccountId.fromString("0.6.9");
+
+ var tx = new RegisteredNodeCreateTransaction()
+ .setNodeAccountIds(Arrays.asList(AccountId.fromString("0.0.3")))
+ .setTransactionId(TransactionId.withValidStart(ACCOUNT_ID, VALID_START))
+ .setAdminKey(TEST_ADMIN_KEY);
+
+ assertThatCode(() -> tx.freezeWith(null)).doesNotThrowAnyException();
+ assertThat(tx.getAdminKey()).isEqualTo(TEST_ADMIN_KEY);
+ }
+
+ @Test
+ void shouldThrowErrorWhenFreezingWithoutAdminKey() {
+ final Instant VALID_START = Instant.ofEpochSecond(1596210382);
+ final AccountId ACCOUNT_ID = AccountId.fromString("0.6.9");
+
+ var tx = new RegisteredNodeCreateTransaction()
+ .setNodeAccountIds(Arrays.asList(AccountId.fromString("0.0.3")))
+ .setTransactionId(TransactionId.withValidStart(ACCOUNT_ID, VALID_START));
+
+ var exception = assertThrows(IllegalStateException.class, () -> tx.freezeWith(null));
+ assertThat(exception.getMessage())
+ .isEqualTo(
+ "RegisteredNodeCreateTransaction: 'adminKey' must be explicitly set before calling freeze().");
+ }
+}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeCreateTransactionTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeCreateTransactionTest.snap
new file mode 100644
index 000000000..da4a6c84e
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeCreateTransactionTest.snap
@@ -0,0 +1,3 @@
+com.hedera.hashgraph.sdk.RegisteredNodeCreateTransactionTest.shouldSerialize=[
+ "# com.hedera.hashgraph.sdk.proto.TransactionBody\nnode_account_i_d {\n account_num: 5005\n realm_num: 0\n shard_num: 0\n}\nregistered_node_create {\n admin_key {\n ed25519: \"\\030\\214\\252`\\231\\024#O\\b\\370\\342\\232\\321g\\274\\273\\346\\221}\\211m\\244R\\306\\017\\230\\017j,\\246\\206\\001\"\n }\n description: \"Test description\"\n service_endpoint {\n block_node {\n endpoint_api: STATUS\n endpoint_api_value: 1\n }\n domain_name: \"example.block.com\"\n port: 443\n requires_tls: true\n }\n service_endpoint {\n block_node {\n endpoint_api: STATUS\n endpoint_api_value: 1\n }\n domain_name: \"example.block.com\"\n port: 444\n requires_tls: true\n }\n service_endpoint {\n block_node {\n endpoint_api: STATUS\n endpoint_api_value: 1\n }\n domain_name: \"example.block.com\"\n port: 445\n requires_tls: true\n }\n}\ntransaction_fee: 100000000\ntransaction_i_d {\n account_i_d {\n account_num: 5006\n realm_num: 0\n shard_num: 0\n }\n transaction_valid_start {\n seconds: 1554158542\n }\n}\ntransaction_valid_duration {\n seconds: 120\n}"
+]
\ No newline at end of file
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeDeleteTransactionTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeDeleteTransactionTest.java
new file mode 100644
index 000000000..679d46db5
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeDeleteTransactionTest.java
@@ -0,0 +1,154 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.hedera.hashgraph.sdk.proto.RegisteredNodeDeleteTransactionBody;
+import com.hedera.hashgraph.sdk.proto.SchedulableTransactionBody;
+import com.hedera.hashgraph.sdk.proto.TransactionBody;
+import io.github.jsonSnapshot.SnapshotMatcher;
+import java.time.Instant;
+import java.util.Arrays;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class RegisteredNodeDeleteTransactionTest {
+ private static final PrivateKey TEST_PRIVATE_KEY = PrivateKey.fromString(
+ "302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c97732394482538e10");
+
+ private static final long TEST_REGISTERED_NODE_ID = 420;
+
+ final Instant TEST_VALID_START = Instant.ofEpochSecond(1554158542);
+
+ @BeforeAll
+ public static void beforeAll() {
+ SnapshotMatcher.start(Snapshot::asJsonString);
+ }
+
+ @AfterAll
+ public static void afterAll() {
+ SnapshotMatcher.validateSnapshots();
+ }
+
+ RegisteredNodeDeleteTransaction spawnTestTransaction() {
+ return new RegisteredNodeDeleteTransaction()
+ .setNodeAccountIds(Arrays.asList(AccountId.fromString("0.0.5005"), AccountId.fromString("0.0.5006")))
+ .setTransactionId(TransactionId.withValidStart(AccountId.fromString("0.0.5006"), TEST_VALID_START))
+ .setRegisteredNodeId(TEST_REGISTERED_NODE_ID)
+ .setMaxTransactionFee(new Hbar(1))
+ .freeze()
+ .sign(TEST_PRIVATE_KEY);
+ }
+
+ @Test
+ void shouldSerialize() {
+ SnapshotMatcher.expect(spawnTestTransaction().toString()).toMatchSnapshot();
+ }
+
+ @Test
+ void shouldBytes() throws Exception {
+ var tx1 = spawnTestTransaction();
+ var tx2 = RegisteredNodeDeleteTransaction.fromBytes(tx1.toBytes());
+ assertThat(tx2.toString()).isEqualTo(tx1.toString());
+ }
+
+ @Test
+ void shouldBytesNoSetters() throws Exception {
+ var tx1 = new RegisteredNodeDeleteTransaction();
+ var tx2 = RegisteredNodeDeleteTransaction.fromBytes(tx1.toBytes());
+ assertThat(tx2.toString()).isEqualTo(tx1.toString());
+ }
+
+ @Test
+ void fromScheduledTransaction() {
+ var transactionBody = SchedulableTransactionBody.newBuilder()
+ .setRegisteredNodeDelete(
+ RegisteredNodeDeleteTransactionBody.newBuilder().build())
+ .build();
+
+ var tx = Transaction.fromScheduledTransaction(transactionBody);
+ assertThat(tx).isInstanceOf(RegisteredNodeDeleteTransaction.class);
+ }
+
+ @Test
+ void constructNodeDeleteTransactionFromTransactionBodyProtobuf() {
+ var transactionBodyBuilder = RegisteredNodeDeleteTransactionBody.newBuilder();
+
+ transactionBodyBuilder.setRegisteredNodeId(TEST_REGISTERED_NODE_ID);
+
+ var tx = TransactionBody.newBuilder()
+ .setRegisteredNodeDelete(transactionBodyBuilder.build())
+ .build();
+ var registeredNodeDeleteTransaction = new RegisteredNodeDeleteTransaction(tx);
+
+ assertThat(registeredNodeDeleteTransaction.getRegisteredNodeId()).isEqualTo(TEST_REGISTERED_NODE_ID);
+ }
+
+ @Test
+ void getSetRegisteredNodeId() {
+ var tx = new RegisteredNodeDeleteTransaction().setRegisteredNodeId(TEST_REGISTERED_NODE_ID);
+ assertThat(tx.getRegisteredNodeId()).isEqualTo(TEST_REGISTERED_NODE_ID);
+ }
+
+ @Test
+ void getSetRegisteredNodeIdFrozen() {
+ var tx = spawnTestTransaction();
+ assertThrows(IllegalStateException.class, () -> tx.setRegisteredNodeId(TEST_REGISTERED_NODE_ID));
+ }
+
+ @Test
+ void shouldThrowErrorWhenGettingRegisteredNodeIdWithoutSettingIt() {
+ var tx = new RegisteredNodeDeleteTransaction();
+
+ var exception = assertThrows(IllegalStateException.class, () -> tx.getRegisteredNodeId());
+ assertThat(exception.getMessage())
+ .isEqualTo("RegisteredNodeDeleteTransaction: 'registeredNodeId' has not been set");
+ }
+
+ @Test
+ void shouldThrowErrorWhenSettingNegativeRegisteredNodeId() {
+ var tx = new RegisteredNodeDeleteTransaction();
+
+ var exception = assertThrows(IllegalArgumentException.class, () -> tx.setRegisteredNodeId(-1));
+ assertThat(exception.getMessage())
+ .isEqualTo("RegisteredNodeDeleteTransaction: 'registeredNodeId' must be non-negative");
+ }
+
+ @Test
+ void shouldAllowSettingRegisteredNodeIdToZero() {
+ var tx = new RegisteredNodeDeleteTransaction().setRegisteredNodeId(0);
+ assertThat(tx.getRegisteredNodeId()).isEqualTo(0);
+ }
+
+ @Test
+ void shouldFreezeSuccessfullyWhenRegisteredNodeIdIsSet() {
+ final Instant VALID_START = Instant.ofEpochSecond(1596210382);
+ final AccountId ACCOUNT_ID = AccountId.fromString("0.6.9");
+
+ var tx = new RegisteredNodeDeleteTransaction()
+ .setNodeAccountIds(Arrays.asList(AccountId.fromString("0.0.3")))
+ .setTransactionId(TransactionId.withValidStart(ACCOUNT_ID, VALID_START))
+ .setRegisteredNodeId(420);
+
+ assertThatCode(() -> tx.freezeWith(null)).doesNotThrowAnyException();
+ assertThat(tx.getRegisteredNodeId()).isEqualTo(420);
+ }
+
+ @Test
+ void shouldThrowErrorWhenFreezingWithZeroRegisteredNodeId() {
+ final Instant VALID_START = Instant.ofEpochSecond(1596210382);
+ final AccountId ACCOUNT_ID = AccountId.fromString("0.6.9");
+
+ var tx = new RegisteredNodeDeleteTransaction()
+ .setNodeAccountIds(Arrays.asList(AccountId.fromString("0.0.3")))
+ .setTransactionId(TransactionId.withValidStart(ACCOUNT_ID, VALID_START));
+
+ var exception = assertThrows(IllegalStateException.class, () -> tx.freezeWith(null));
+ assertThat(exception.getMessage())
+ .isEqualTo(
+ "RegisteredNodeDeleteTransaction: 'registeredNodeId' must be explicitly set before calling freeze().");
+ }
+}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeDeleteTransactionTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeDeleteTransactionTest.snap
new file mode 100644
index 000000000..fe0c1e545
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeDeleteTransactionTest.snap
@@ -0,0 +1,3 @@
+com.hedera.hashgraph.sdk.RegisteredNodeDeleteTransactionTest.shouldSerialize=[
+ "# com.hedera.hashgraph.sdk.proto.TransactionBody\nnode_account_i_d {\n account_num: 5005\n realm_num: 0\n shard_num: 0\n}\nregistered_node_delete {\n registered_node_id: 420\n}\ntransaction_fee: 100000000\ntransaction_i_d {\n account_i_d {\n account_num: 5006\n realm_num: 0\n shard_num: 0\n }\n transaction_valid_start {\n seconds: 1554158542\n }\n}\ntransaction_valid_duration {\n seconds: 120\n}"
+]
\ No newline at end of file
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeTest.java
new file mode 100644
index 000000000..599934e72
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeTest.java
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import io.github.jsonSnapshot.SnapshotMatcher;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class RegisteredNodeTest {
+ private static final PrivateKey privateKey = PrivateKey.fromString(
+ "302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c97732394482538e10");
+
+ private final BlockNodeServiceEndpoint serviceEndpoint = new BlockNodeServiceEndpoint()
+ .addEndpointApi(BlockNodeApi.STATUS)
+ .setPort(443)
+ .setDomainName("test.block.com")
+ .setRequiresTls(true);
+
+ private final com.hedera.hashgraph.sdk.proto.RegisteredNode registeredNode =
+ com.hedera.hashgraph.sdk.proto.RegisteredNode.newBuilder()
+ .setRegisteredNodeId(1)
+ .setDescription("Unit test registered node")
+ .setAdminKey(privateKey.getPublicKey().toProtobufKey())
+ .addServiceEndpoint(serviceEndpoint.toProtobuf())
+ .build();
+
+ @BeforeAll
+ public static void beforeAll() {
+ SnapshotMatcher.start(Snapshot::asJsonString);
+ }
+
+ @AfterAll
+ public static void afterAll() {
+ SnapshotMatcher.validateSnapshots();
+ }
+
+ @Test
+ void fromProtobuf() {
+ SnapshotMatcher.expect(RegisteredNode.fromProtobuf(registeredNode).toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void fromBytes() throws InvalidProtocolBufferException {
+ SnapshotMatcher.expect(
+ RegisteredNode.fromBytes(registeredNode.toByteArray()).toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void toBytes() throws InvalidProtocolBufferException {
+ SnapshotMatcher.expect(
+ RegisteredNode.fromBytes(registeredNode.toByteArray()).toBytes())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void toProtobuf() throws InvalidProtocolBufferException {
+ SnapshotMatcher.expect(
+ RegisteredNode.fromProtobuf(registeredNode).toProtobuf().toString())
+ .toMatchSnapshot();
+ }
+}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeTest.snap
new file mode 100644
index 000000000..b1bcf59d5
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeTest.snap
@@ -0,0 +1,18 @@
+com.hedera.hashgraph.sdk.RegisteredNodeTest.fromBytes=[
+ "RegisteredNode{registeredNodeId=1, adminKey=302a300506032b6570032100e0c8ec2758a5879ffac226a13c0c516b799e72e35141a0dd828f94d37988a4b7, description=Unit test registered node, serviceEndpoints=[BlockNodeServiceEndpoint{ipAddress=null, domainName=test.block.com, port=443, requiresTls=true, endpointApis=[STATUS]}]}"
+]
+
+
+com.hedera.hashgraph.sdk.RegisteredNodeTest.fromProtobuf=[
+ "RegisteredNode{registeredNodeId=1, adminKey=302a300506032b6570032100e0c8ec2758a5879ffac226a13c0c516b799e72e35141a0dd828f94d37988a4b7, description=Unit test registered node, serviceEndpoints=[BlockNodeServiceEndpoint{ipAddress=null, domainName=test.block.com, port=443, requiresTls=true, endpointApis=[STATUS]}]}"
+]
+
+
+com.hedera.hashgraph.sdk.RegisteredNodeTest.toBytes=[
+ "CAESIhIg4MjsJ1ilh5/6wiahPAxRa3mecuNRQaDdgo+U03mIpLcaGVVuaXQgdGVzdCByZWdpc3RlcmVkIG5vZGUiGhIOdGVzdC5ibG9jay5jb20YuwMgASoDCgEB"
+]
+
+
+com.hedera.hashgraph.sdk.RegisteredNodeTest.toProtobuf=[
+ "# com.hedera.hashgraph.sdk.proto.RegisteredNode@7c42c1c6\nadmin_key {\n ed25519: \"\\340\\310\\354\\'X\\245\\207\\237\\372\\302&\\241<\\fQky\\236r\\343QA\\240\\335\\202\\217\\224\\323y\\210\\244\\267\"\n}\ndescription: \"Unit test registered node\"\nregistered_node_id: 1\nservice_endpoint {\n block_node {\n endpoint_api: STATUS\n endpoint_api_value: 1\n }\n domain_name: \"test.block.com\"\n port: 443\n requires_tls: true\n}"
+]
\ No newline at end of file
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeUpdateTransactionTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeUpdateTransactionTest.java
new file mode 100644
index 000000000..05230b8bf
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeUpdateTransactionTest.java
@@ -0,0 +1,262 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.google.protobuf.StringValue;
+import com.hedera.hashgraph.sdk.proto.RegisteredNodeUpdateTransactionBody;
+import com.hedera.hashgraph.sdk.proto.SchedulableTransactionBody;
+import com.hedera.hashgraph.sdk.proto.TransactionBody;
+import io.github.jsonSnapshot.SnapshotMatcher;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class RegisteredNodeUpdateTransactionTest {
+ private static final PrivateKey TEST_PRIVATE_KEY = PrivateKey.fromString(
+ "302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c97732394482538e10");
+
+ private static final AccountId TEST_ACCOUNT_ID = AccountId.fromString("0.6.9");
+
+ private static final String TEST_DESCRIPTION = "Test description";
+
+ private static final long TEST_REGISTERED_NODE_ID = 43;
+
+ private static final List TEST_SERVICE_ENDPOINT =
+ List.of(spawnTestEndpoint((byte) 0), spawnTestEndpoint((byte) 1), spawnTestEndpoint((byte) 2));
+
+ private static final PublicKey TEST_ADMIN_KEY = PrivateKey.fromString(
+ "302e020100300506032b65700422042062c4b69e9f45a554e5424fb5a6fe5e6ac1f19ead31dc7718c2d980fd1f998d4b")
+ .getPublicKey();
+
+ final Instant TEST_VALID_START = Instant.ofEpochSecond(1554158542);
+
+ static RegisteredServiceEndpoint spawnTestEndpoint(byte offset) {
+ return new BlockNodeServiceEndpoint()
+ .setDomainName("example.block.com")
+ .setPort(443 + offset)
+ .setRequiresTls(true)
+ .addEndpointApi(BlockNodeApi.STATUS);
+ }
+
+ RegisteredNodeUpdateTransaction spawnTestTransaction() {
+ return new RegisteredNodeUpdateTransaction()
+ .setNodeAccountIds(Arrays.asList(AccountId.fromString("0.0.5005"), AccountId.fromString("0.0.5006")))
+ .setTransactionId(TransactionId.withValidStart(AccountId.fromString("0.0.5006"), TEST_VALID_START))
+ .setRegisteredNodeId(TEST_REGISTERED_NODE_ID)
+ .setAdminKey(TEST_ADMIN_KEY)
+ .setDescription(TEST_DESCRIPTION)
+ .setServiceEndpoints(TEST_SERVICE_ENDPOINT)
+ .setMaxTransactionFee(new Hbar(1))
+ .freeze()
+ .sign(TEST_PRIVATE_KEY);
+ }
+
+ @BeforeAll
+ public static void beforeAll() {
+ SnapshotMatcher.start(Snapshot::asJsonString);
+ }
+
+ @AfterAll
+ public static void afterAll() {
+ SnapshotMatcher.validateSnapshots();
+ }
+
+ @Test
+ void shouldSerialize() {
+ SnapshotMatcher.expect(spawnTestTransaction().toString()).toMatchSnapshot();
+ }
+
+ @Test
+ void shouldBytes() throws Exception {
+ var tx1 = spawnTestTransaction();
+ var tx2 = RegisteredNodeUpdateTransaction.fromBytes(tx1.toBytes());
+ assertThat(tx2.toString()).isEqualTo(tx1.toString());
+ }
+
+ @Test
+ void shouldBytesNoSetters() throws Exception {
+ var tx1 = new RegisteredNodeUpdateTransaction();
+ var tx2 = Transaction.fromBytes(tx1.toBytes());
+ assertThat(tx2.toString()).isEqualTo(tx1.toString());
+ }
+
+ @Test
+ void fromScheduledTransaction() {
+ var transactionBody = SchedulableTransactionBody.newBuilder()
+ .setRegisteredNodeUpdate(
+ RegisteredNodeUpdateTransactionBody.newBuilder().build())
+ .build();
+
+ var tx = Transaction.fromScheduledTransaction(transactionBody);
+
+ assertThat(tx).isInstanceOf(RegisteredNodeUpdateTransaction.class);
+ }
+
+ @Test
+ void getRegisterNodeIdThrowErrorWhenNotSet() {
+ var tx = new RegisteredNodeUpdateTransaction();
+ var exception = assertThrows(IllegalStateException.class, () -> tx.getRegisteredNodeId());
+ assertThat(exception.getMessage())
+ .isEqualTo("RegisteredNodeUpdateTransaction: 'registeredNodeId' has not been set");
+ }
+
+ @Test
+ void setRegisteredNodeId() {
+ var tx = new RegisteredNodeUpdateTransaction().setRegisteredNodeId(TEST_REGISTERED_NODE_ID);
+ assertThat(tx.getRegisteredNodeId()).isEqualTo(TEST_REGISTERED_NODE_ID);
+ }
+
+ @Test
+ void setRegisteredNodeIdFrozen() {
+ var tx = spawnTestTransaction();
+ assertThrows(IllegalStateException.class, () -> tx.setRegisteredNodeId(TEST_REGISTERED_NODE_ID));
+ }
+
+ @Test
+ void setRegisteredNodeIdValueLessThanZero() {
+ var tx = new RegisteredNodeUpdateTransaction();
+ assertThrows(IllegalArgumentException.class, () -> tx.setRegisteredNodeId(-1));
+ }
+
+ @Test
+ void setAdminKey() {
+ var tx = new RegisteredNodeUpdateTransaction().setAdminKey(TEST_ADMIN_KEY);
+ assertThat(tx.getAdminKey()).isEqualTo(TEST_ADMIN_KEY);
+ }
+
+ @Test
+ void setAdminKeyFrozen() {
+ var tx = spawnTestTransaction();
+ assertThrows(IllegalStateException.class, () -> tx.setAdminKey(TEST_ADMIN_KEY));
+ }
+
+ @Test
+ void setDescription() {
+ var tx = new RegisteredNodeUpdateTransaction().setDescription(TEST_DESCRIPTION);
+ assertThat(tx.getDescription()).isEqualTo(TEST_DESCRIPTION);
+ }
+
+ @Test
+ void setDescriptionFrozen() {
+ var tx = spawnTestTransaction();
+ assertThrows(IllegalStateException.class, () -> tx.setDescription(TEST_DESCRIPTION));
+ }
+
+ @Test
+ void setDescriptionRejectsOver100Utf8Bytes() {
+ var tx = new RegisteredNodeUpdateTransaction();
+ String tooLong = "a".repeat(101);
+ assertThrows(IllegalArgumentException.class, () -> tx.setDescription(tooLong));
+ }
+
+ @Test
+ void setDescriptionAcceptsExactly100Utf8Bytes() {
+ var tx = new RegisteredNodeUpdateTransaction();
+ String exact = "a".repeat(100);
+ tx.setDescription(exact);
+ assertThat(tx.getDescription()).isEqualTo(exact);
+ }
+
+ @Test
+ void setServiceEndpoint() {
+ var tx = new RegisteredNodeUpdateTransaction().setServiceEndpoints(TEST_SERVICE_ENDPOINT);
+ assertThat(tx.getServiceEndpoints()).hasSize(TEST_SERVICE_ENDPOINT.size());
+ assertThat(tx.getServiceEndpoints()).isEqualTo(TEST_SERVICE_ENDPOINT);
+ }
+
+ @Test
+ void setServiceEndpointFrozen() {
+ var tx = spawnTestTransaction();
+ assertThrows(IllegalStateException.class, () -> tx.setServiceEndpoints(TEST_SERVICE_ENDPOINT));
+ }
+
+ @Test
+ void setServiceEndpointRejectsMoreThan50() {
+ var tx = new RegisteredNodeUpdateTransaction();
+ var serviceEndpoints = new ArrayList();
+ for (int i = 0; i < 51; i++) {
+ serviceEndpoints.add(spawnTestEndpoint((byte) i));
+ }
+
+ assertThrows(IllegalArgumentException.class, () -> tx.setServiceEndpoints(serviceEndpoints));
+ }
+
+ @Test
+ void addServiceEndpoint() {
+ var tx = new RegisteredNodeUpdateTransaction();
+ var serviceEndpoint = spawnTestEndpoint((byte) 1);
+
+ tx.addServiceEndpoint(serviceEndpoint);
+ assertThat(tx.getServiceEndpoints()).hasSize(1);
+ assertThat(tx.getServiceEndpoints().get(0)).isEqualTo(serviceEndpoint);
+ }
+
+ @Test
+ void addServiceEndpointRejectsMoreThan50() {
+ var tx = new RegisteredNodeUpdateTransaction();
+ for (int i = 0; i < 50; i++) {
+ tx.addServiceEndpoint(spawnTestEndpoint((byte) i));
+ }
+
+ assertThrows(IllegalArgumentException.class, () -> tx.addServiceEndpoint(spawnTestEndpoint((byte) 50)));
+ }
+
+ @Test
+ void constructRegisteredNodeUpdateTransactionFromTransactionBodyProtobuf() {
+ var transactionBodyBuilder = RegisteredNodeUpdateTransactionBody.newBuilder();
+
+ transactionBodyBuilder.setRegisteredNodeId(TEST_REGISTERED_NODE_ID);
+ transactionBodyBuilder.setAdminKey(TEST_ADMIN_KEY.toProtobufKey());
+ transactionBodyBuilder.setDescription(StringValue.of(TEST_DESCRIPTION));
+
+ for (RegisteredServiceEndpoint serviceEndpoint : TEST_SERVICE_ENDPOINT) {
+ transactionBodyBuilder.addServiceEndpoint(serviceEndpoint.toProtobuf());
+ }
+
+ var transactionBody = TransactionBody.newBuilder()
+ .setRegisteredNodeUpdate(transactionBodyBuilder.build())
+ .build();
+ var tx = new RegisteredNodeUpdateTransaction(transactionBody);
+
+ assertThat(tx.getRegisteredNodeId()).isEqualTo(TEST_REGISTERED_NODE_ID);
+ assertThat(tx.getAdminKey()).isEqualTo(TEST_ADMIN_KEY);
+ assertThat(tx.getDescription()).isEqualTo(TEST_DESCRIPTION);
+ assertThat(tx.getServiceEndpoints()).hasSize(TEST_SERVICE_ENDPOINT.size());
+ }
+
+ @Test
+ void shouldFreezeSuccessfullyWhenRegisteredNodeIdSet() {
+ final Instant VALID_START = Instant.ofEpochSecond(1596210382);
+ final AccountId ACCOUNT_ID = AccountId.fromString("0.6.9");
+
+ var tx = new RegisteredNodeUpdateTransaction()
+ .setNodeAccountIds(Arrays.asList(AccountId.fromString("0.0.3")))
+ .setTransactionId(TransactionId.withValidStart(ACCOUNT_ID, VALID_START))
+ .setRegisteredNodeId(TEST_REGISTERED_NODE_ID);
+
+ assertThatCode(() -> tx.freezeWith(null)).doesNotThrowAnyException();
+ assertThat(tx.getRegisteredNodeId()).isEqualTo(TEST_REGISTERED_NODE_ID);
+ }
+
+ @Test
+ void shouldThrowErrorWhenFreezingWithoutRegisteredNodeId() {
+ final Instant VALID_START = Instant.ofEpochSecond(1596210382);
+ final AccountId ACCOUNT_ID = AccountId.fromString("0.6.9");
+
+ var tx = new RegisteredNodeUpdateTransaction()
+ .setNodeAccountIds(Arrays.asList(AccountId.fromString("0.0.3")))
+ .setTransactionId(TransactionId.withValidStart(ACCOUNT_ID, VALID_START));
+
+ var exception = assertThrows(IllegalStateException.class, () -> tx.freezeWith(null));
+ assertThat(exception.getMessage())
+ .isEqualTo(
+ "RegisteredNodeUpdateTransaction: 'registeredNodeId' must be explicitly set before calling freeze().");
+ }
+}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeUpdateTransactionTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeUpdateTransactionTest.snap
new file mode 100644
index 000000000..416604030
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/RegisteredNodeUpdateTransactionTest.snap
@@ -0,0 +1,3 @@
+com.hedera.hashgraph.sdk.RegisteredNodeUpdateTransactionTest.shouldSerialize=[
+ "# com.hedera.hashgraph.sdk.proto.TransactionBody\nnode_account_i_d {\n account_num: 5005\n realm_num: 0\n shard_num: 0\n}\nregistered_node_update {\n admin_key {\n ed25519: \"\\030\\214\\252`\\231\\024#O\\b\\370\\342\\232\\321g\\274\\273\\346\\221}\\211m\\244R\\306\\017\\230\\017j,\\246\\206\\001\"\n }\n description {\n value: \"Test description\"\n }\n registered_node_id: 43\n service_endpoint {\n block_node {\n endpoint_api: STATUS\n endpoint_api_value: 1\n }\n domain_name: \"example.block.com\"\n port: 443\n requires_tls: true\n }\n service_endpoint {\n block_node {\n endpoint_api: STATUS\n endpoint_api_value: 1\n }\n domain_name: \"example.block.com\"\n port: 444\n requires_tls: true\n }\n service_endpoint {\n block_node {\n endpoint_api: STATUS\n endpoint_api_value: 1\n }\n domain_name: \"example.block.com\"\n port: 445\n requires_tls: true\n }\n}\ntransaction_fee: 100000000\ntransaction_i_d {\n account_i_d {\n account_num: 5006\n realm_num: 0\n shard_num: 0\n }\n transaction_valid_start {\n seconds: 1554158542\n }\n}\ntransaction_valid_duration {\n seconds: 120\n}"
+]
\ No newline at end of file
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/RpcRelayServiceEndpointTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/RpcRelayServiceEndpointTest.java
new file mode 100644
index 000000000..f78cb6ed3
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/RpcRelayServiceEndpointTest.java
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import com.google.protobuf.ByteString;
+import com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint;
+import io.github.jsonSnapshot.SnapshotMatcher;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class RpcRelayServiceEndpointTest {
+ private final byte[] TEST_IP_ADDRESS = new byte[] {1, 2, 3, 4};
+ private final String TEST_DOMAIN_NAME = "test.mirror.com";
+ private final int TEST_PORT = 443;
+ private final boolean TEST_REQUIRES_TLS = true;
+
+ private final com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint rpcEndpointWithDomain =
+ com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint.newBuilder()
+ .setDomainName(TEST_DOMAIN_NAME)
+ .setPort(TEST_PORT)
+ .setRequiresTls(TEST_REQUIRES_TLS)
+ .setRpcRelay(RegisteredServiceEndpoint.RpcRelayEndpoint.newBuilder())
+ .build();
+
+ private final com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint rpcEndpointWithIp =
+ com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint.newBuilder()
+ .setIpAddress(ByteString.copyFrom(TEST_IP_ADDRESS))
+ .setPort(TEST_PORT)
+ .setRequiresTls(TEST_REQUIRES_TLS)
+ .setRpcRelay(RegisteredServiceEndpoint.RpcRelayEndpoint.newBuilder())
+ .build();
+
+ @BeforeAll
+ public static void beforeAll() {
+ SnapshotMatcher.start(Snapshot::asJsonString);
+ }
+
+ @AfterAll
+ public static void afterAll() {
+ SnapshotMatcher.validateSnapshots();
+ }
+
+ @Test
+ void fromProtobufWithDomain() {
+ SnapshotMatcher.expect(RpcRelayServiceEndpoint.fromProtobuf(rpcEndpointWithDomain)
+ .toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void toProtobufWithDomain() {
+ SnapshotMatcher.expect(RpcRelayServiceEndpoint.fromProtobuf(rpcEndpointWithDomain)
+ .toProtobuf()
+ .toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void fromProtobufWithIp() {
+ SnapshotMatcher.expect(
+ RpcRelayServiceEndpoint.fromProtobuf(rpcEndpointWithIp).toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void toProtobufWithIp() {
+ SnapshotMatcher.expect(RpcRelayServiceEndpoint.fromProtobuf(rpcEndpointWithIp)
+ .toProtobuf()
+ .toString())
+ .toMatchSnapshot();
+ }
+
+ @Test
+ void setIpAddress() {
+ var endpoint = new BlockNodeServiceEndpoint().setIpAddress(TEST_IP_ADDRESS);
+ assertThat(endpoint.getIpAddress()).isEqualTo(TEST_IP_ADDRESS);
+ }
+
+ @Test
+ void setDomainName() {
+ var endpoint = new BlockNodeServiceEndpoint().setDomainName(TEST_DOMAIN_NAME);
+ assertThat(endpoint.getDomainName()).isEqualTo(TEST_DOMAIN_NAME);
+ }
+
+ @Test
+ void setPort() {
+ var endpoint = new BlockNodeServiceEndpoint().setPort(TEST_PORT);
+ assertThat(endpoint.getPort()).isEqualTo(TEST_PORT);
+ }
+
+ @Test
+ void setPortThrowsOnNegative() {
+ var endpoint = new BlockNodeServiceEndpoint();
+ assertThatThrownBy(() -> endpoint.setPort(-1)).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void setPortThrowsOnGreaterThan65535() {
+ var endpoint = new BlockNodeServiceEndpoint();
+ assertThatThrownBy(() -> endpoint.setPort(65536)).isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void setRequiresTls() {
+ var endpoint = new BlockNodeServiceEndpoint().setRequiresTls(TEST_REQUIRES_TLS);
+ assertThat(endpoint.isRequiresTls()).isEqualTo(TEST_REQUIRES_TLS);
+ }
+}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/RpcRelayServiceEndpointTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/RpcRelayServiceEndpointTest.snap
new file mode 100644
index 000000000..49c91ffe5
--- /dev/null
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/RpcRelayServiceEndpointTest.snap
@@ -0,0 +1,18 @@
+com.hedera.hashgraph.sdk.RpcRelayServiceEndpointTest.fromProtobufWithDomain=[
+ "RpcRelayServiceEndpoint{ipAddress=null, domainName=test.mirror.com, port=443, requiresTls=true}"
+]
+
+
+com.hedera.hashgraph.sdk.RpcRelayServiceEndpointTest.fromProtobufWithIp=[
+ "RpcRelayServiceEndpoint{ipAddress=[1, 2, 3, 4], domainName=null, port=443, requiresTls=true}"
+]
+
+
+com.hedera.hashgraph.sdk.RpcRelayServiceEndpointTest.toProtobufWithDomain=[
+ "# com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint@28438c0e\ndomain_name: \"test.mirror.com\"\nport: 443\nrequires_tls: true\nrpc_relay {\n}"
+]
+
+
+com.hedera.hashgraph.sdk.RpcRelayServiceEndpointTest.toProtobufWithIp=[
+ "# com.hedera.hashgraph.sdk.proto.RegisteredServiceEndpoint@86e8926\nip_address: \"\\001\\002\\003\\004\"\nport: 443\nrequires_tls: true\nrpc_relay {\n}"
+]
\ No newline at end of file
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionReceiptTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionReceiptTest.java
index a1744695a..7d00243a1 100644
--- a/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionReceiptTest.java
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionReceiptTest.java
@@ -44,6 +44,7 @@ static TransactionReceipt spawnReceiptExample() {
TransactionId.withValidStart(AccountId.fromString("3.3.3"), time),
List.of(1L, 2L, 3L),
1,
+ 1,
new ArrayList<>(),
new ArrayList<>());
}
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionReceiptTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionReceiptTest.snap
index ffd1cfbf2..2feaddc35 100644
--- a/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionReceiptTest.snap
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionReceiptTest.snap
@@ -1,3 +1,3 @@
com.hedera.hashgraph.sdk.TransactionReceiptTest.shouldSerialize=[
- "TransactionReceipt{transactionId=null, status=SCHEDULE_ALREADY_DELETED, exchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, nextExchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, accountId=1.2.3, fileId=4.5.6, contractId=3.2.1, topicId=9.8.7, tokenId=6.5.4, topicSequenceNumber=3, topicRunningHash=[54, 56, 54, 102, 55, 55, 50, 48, 54, 101, 54, 102, 55, 55, 50, 48, 54, 50, 55, 50, 54, 102, 55, 55, 54, 101, 50, 48, 54, 51, 54, 102, 55, 55], totalSupply=30, scheduleId=1.1.1, scheduledTransactionId=3.3.3@1554158542.000000000, serials=[1, 2, 3], nodeId=1, duplicates=[], children=[]}"
-]
+ "TransactionReceipt{transactionId=null, status=SCHEDULE_ALREADY_DELETED, exchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, nextExchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, accountId=1.2.3, fileId=4.5.6, contractId=3.2.1, topicId=9.8.7, tokenId=6.5.4, topicSequenceNumber=3, topicRunningHash=[54, 56, 54, 102, 55, 55, 50, 48, 54, 101, 54, 102, 55, 55, 50, 48, 54, 50, 55, 50, 54, 102, 55, 55, 54, 101, 50, 48, 54, 51, 54, 102, 55, 55], totalSupply=30, scheduleId=1.1.1, scheduledTransactionId=3.3.3@1554158542.000000000, serials=[1, 2, 3], nodeId=1, registeredNodeId=1, duplicates=[], children=[]}"
+]
\ No newline at end of file
diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionRecordTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionRecordTest.snap
index c3ccf38f4..fb89c30f9 100644
--- a/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionRecordTest.snap
+++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionRecordTest.snap
@@ -1,8 +1,8 @@
com.hedera.hashgraph.sdk.TransactionRecordTest.shouldSerialize2=[
- "TransactionRecord{receipt=TransactionReceipt{transactionId=null, status=SCHEDULE_ALREADY_DELETED, exchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, nextExchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, accountId=1.2.3, fileId=4.5.6, contractId=3.2.1, topicId=9.8.7, tokenId=6.5.4, topicSequenceNumber=3, topicRunningHash=[54, 56, 54, 102, 55, 55, 50, 48, 54, 101, 54, 102, 55, 55, 50, 48, 54, 50, 55, 50, 54, 102, 55, 55, 54, 101, 50, 48, 54, 51, 54, 102, 55, 55], totalSupply=30, scheduleId=1.1.1, scheduledTransactionId=3.3.3@1554158542.000000000, serials=[1, 2, 3], nodeId=1, duplicates=[], children=[]}, transactionHash=68656c6c6f, consensusTimestamp=2019-04-01T22:42:22Z, transactionId=3.3.3@1554158542.000000000, transactionMemo=memo, transactionFee=3000 tℏ, contractFunctionResult=ContractFunctionResult{contractId=1.2.3, evmAddress=1.2.98329e006610472e6b372c080833f6d79ed833cf, errorMessage=null, bloom=, gasUsed=0, logs=[], createdContractIds=[], stateChanges=[], gas=0, hbarAmount=0 tℏ, contractFunctionparametersBytes=, rawResult=00000000000000000000000000000000000000000000000000000000ffffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000011223344556677889900aabbccddeeff00112233ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001448656c6c6f2c20776f726c642c20616761696e21000000000000000000000000, senderAccountId=1.2.3, contractNonces=[], signerNonce=0}, transfers=[Transfer{accountId=4.4.4, amount=5 ℏ}], tokenTransfers={6.6.6={1.1.1=4}}, tokenNftTransfers={4.4.4=[TokenNftTransfer{tokenId=4.4.4, sender=1.2.3, receiver=3.2.1, serial=4, isApproved=true}]}, scheduleRef=3.3.3, assessedCustomFees=[AssessedCustomFee{amount=4, tokenId=4.5.6, feeCollectorAccountId=8.6.5, payerAccountIdList=[3.3.3]}], automaticTokenAssociations=[TokenAssociation{tokenId=5.4.3, accountId=8.7.6}], aliasKey=3036301006072a8648ce3d020106052b8104000a03220002703a9370b0443be6ae7c507b0aec81a55e94e4a863b9655360bd65358caa6588, children=[], duplicates=[], parentConsensusTimestamp=2019-04-01T22:42:22Z, ethereumHash=536f6d652068617368, paidStakingRewards=[Transfer{accountId=1.2.3, amount=8 ℏ}], prngBytes=null, prngNumber=4, evmAddress=30783030, pendingAirdropRecords=[PendingAirdropRecord{pendingAirdropId=PendingAirdropId{sender=0.0.123, receiver=0.0.124, tokenId=null, nftId=0.0.5005/1234}, pendingAirdropAmount=123}, PendingAirdropRecord{pendingAirdropId=PendingAirdropId{sender=0.0.123, receiver=0.0.124, tokenId=0.0.12345, nftId=null}, pendingAirdropAmount=123}], highVolumePricingMultiplier=1000}"
+ "TransactionRecord{receipt=TransactionReceipt{transactionId=null, status=SCHEDULE_ALREADY_DELETED, exchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, nextExchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, accountId=1.2.3, fileId=4.5.6, contractId=3.2.1, topicId=9.8.7, tokenId=6.5.4, topicSequenceNumber=3, topicRunningHash=[54, 56, 54, 102, 55, 55, 50, 48, 54, 101, 54, 102, 55, 55, 50, 48, 54, 50, 55, 50, 54, 102, 55, 55, 54, 101, 50, 48, 54, 51, 54, 102, 55, 55], totalSupply=30, scheduleId=1.1.1, scheduledTransactionId=3.3.3@1554158542.000000000, serials=[1, 2, 3], nodeId=1, registeredNodeId=1, duplicates=[], children=[]}, transactionHash=68656c6c6f, consensusTimestamp=2019-04-01T22:42:22Z, transactionId=3.3.3@1554158542.000000000, transactionMemo=memo, transactionFee=3000 tℏ, contractFunctionResult=ContractFunctionResult{contractId=1.2.3, evmAddress=1.2.98329e006610472e6b372c080833f6d79ed833cf, errorMessage=null, bloom=, gasUsed=0, logs=[], createdContractIds=[], stateChanges=[], gas=0, hbarAmount=0 tℏ, contractFunctionparametersBytes=, rawResult=00000000000000000000000000000000000000000000000000000000ffffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000011223344556677889900aabbccddeeff00112233ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001448656c6c6f2c20776f726c642c20616761696e21000000000000000000000000, senderAccountId=1.2.3, contractNonces=[], signerNonce=0}, transfers=[Transfer{accountId=4.4.4, amount=5 ℏ}], tokenTransfers={6.6.6={1.1.1=4}}, tokenNftTransfers={4.4.4=[TokenNftTransfer{tokenId=4.4.4, sender=1.2.3, receiver=3.2.1, serial=4, isApproved=true}]}, scheduleRef=3.3.3, assessedCustomFees=[AssessedCustomFee{amount=4, tokenId=4.5.6, feeCollectorAccountId=8.6.5, payerAccountIdList=[3.3.3]}], automaticTokenAssociations=[TokenAssociation{tokenId=5.4.3, accountId=8.7.6}], aliasKey=3036301006072a8648ce3d020106052b8104000a03220002703a9370b0443be6ae7c507b0aec81a55e94e4a863b9655360bd65358caa6588, children=[], duplicates=[], parentConsensusTimestamp=2019-04-01T22:42:22Z, ethereumHash=536f6d652068617368, paidStakingRewards=[Transfer{accountId=1.2.3, amount=8 ℏ}], prngBytes=null, prngNumber=4, evmAddress=30783030, pendingAirdropRecords=[PendingAirdropRecord{pendingAirdropId=PendingAirdropId{sender=0.0.123, receiver=0.0.124, tokenId=null, nftId=0.0.5005/1234}, pendingAirdropAmount=123}, PendingAirdropRecord{pendingAirdropId=PendingAirdropId{sender=0.0.123, receiver=0.0.124, tokenId=0.0.12345, nftId=null}, pendingAirdropAmount=123}], highVolumePricingMultiplier=1000}"
]
com.hedera.hashgraph.sdk.TransactionRecordTest.shouldSerialize=[
- "TransactionRecord{receipt=TransactionReceipt{transactionId=null, status=SCHEDULE_ALREADY_DELETED, exchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, nextExchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, accountId=1.2.3, fileId=4.5.6, contractId=3.2.1, topicId=9.8.7, tokenId=6.5.4, topicSequenceNumber=3, topicRunningHash=[54, 56, 54, 102, 55, 55, 50, 48, 54, 101, 54, 102, 55, 55, 50, 48, 54, 50, 55, 50, 54, 102, 55, 55, 54, 101, 50, 48, 54, 51, 54, 102, 55, 55], totalSupply=30, scheduleId=1.1.1, scheduledTransactionId=3.3.3@1554158542.000000000, serials=[1, 2, 3], nodeId=1, duplicates=[], children=[]}, transactionHash=68656c6c6f, consensusTimestamp=2019-04-01T22:42:22Z, transactionId=3.3.3@1554158542.000000000, transactionMemo=memo, transactionFee=3000 tℏ, contractFunctionResult=ContractFunctionResult{contractId=1.2.3, evmAddress=1.2.98329e006610472e6b372c080833f6d79ed833cf, errorMessage=null, bloom=, gasUsed=0, logs=[], createdContractIds=[], stateChanges=[], gas=0, hbarAmount=0 tℏ, contractFunctionparametersBytes=, rawResult=00000000000000000000000000000000000000000000000000000000ffffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000011223344556677889900aabbccddeeff00112233ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001448656c6c6f2c20776f726c642c20616761696e21000000000000000000000000, senderAccountId=1.2.3, contractNonces=[], signerNonce=0}, transfers=[Transfer{accountId=4.4.4, amount=5 ℏ}], tokenTransfers={6.6.6={1.1.1=4}}, tokenNftTransfers={4.4.4=[TokenNftTransfer{tokenId=4.4.4, sender=1.2.3, receiver=3.2.1, serial=4, isApproved=true}]}, scheduleRef=3.3.3, assessedCustomFees=[AssessedCustomFee{amount=4, tokenId=4.5.6, feeCollectorAccountId=8.6.5, payerAccountIdList=[3.3.3]}], automaticTokenAssociations=[TokenAssociation{tokenId=5.4.3, accountId=8.7.6}], aliasKey=3036301006072a8648ce3d020106052b8104000a03220002703a9370b0443be6ae7c507b0aec81a55e94e4a863b9655360bd65358caa6588, children=[], duplicates=[], parentConsensusTimestamp=2019-04-01T22:42:22Z, ethereumHash=536f6d652068617368, paidStakingRewards=[Transfer{accountId=1.2.3, amount=8 ℏ}], prngBytes=766572792072616e646f6d206279746573, prngNumber=null, evmAddress=30783030, pendingAirdropRecords=[PendingAirdropRecord{pendingAirdropId=PendingAirdropId{sender=0.0.123, receiver=0.0.124, tokenId=null, nftId=0.0.5005/1234}, pendingAirdropAmount=123}, PendingAirdropRecord{pendingAirdropId=PendingAirdropId{sender=0.0.123, receiver=0.0.124, tokenId=0.0.12345, nftId=null}, pendingAirdropAmount=123}], highVolumePricingMultiplier=1000}"
+ "TransactionRecord{receipt=TransactionReceipt{transactionId=null, status=SCHEDULE_ALREADY_DELETED, exchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, nextExchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, accountId=1.2.3, fileId=4.5.6, contractId=3.2.1, topicId=9.8.7, tokenId=6.5.4, topicSequenceNumber=3, topicRunningHash=[54, 56, 54, 102, 55, 55, 50, 48, 54, 101, 54, 102, 55, 55, 50, 48, 54, 50, 55, 50, 54, 102, 55, 55, 54, 101, 50, 48, 54, 51, 54, 102, 55, 55], totalSupply=30, scheduleId=1.1.1, scheduledTransactionId=3.3.3@1554158542.000000000, serials=[1, 2, 3], nodeId=1, registeredNodeId=1, duplicates=[], children=[]}, transactionHash=68656c6c6f, consensusTimestamp=2019-04-01T22:42:22Z, transactionId=3.3.3@1554158542.000000000, transactionMemo=memo, transactionFee=3000 tℏ, contractFunctionResult=ContractFunctionResult{contractId=1.2.3, evmAddress=1.2.98329e006610472e6b372c080833f6d79ed833cf, errorMessage=null, bloom=, gasUsed=0, logs=[], createdContractIds=[], stateChanges=[], gas=0, hbarAmount=0 tℏ, contractFunctionparametersBytes=, rawResult=00000000000000000000000000000000000000000000000000000000ffffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000011223344556677889900aabbccddeeff00112233ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001448656c6c6f2c20776f726c642c20616761696e21000000000000000000000000, senderAccountId=1.2.3, contractNonces=[], signerNonce=0}, transfers=[Transfer{accountId=4.4.4, amount=5 ℏ}], tokenTransfers={6.6.6={1.1.1=4}}, tokenNftTransfers={4.4.4=[TokenNftTransfer{tokenId=4.4.4, sender=1.2.3, receiver=3.2.1, serial=4, isApproved=true}]}, scheduleRef=3.3.3, assessedCustomFees=[AssessedCustomFee{amount=4, tokenId=4.5.6, feeCollectorAccountId=8.6.5, payerAccountIdList=[3.3.3]}], automaticTokenAssociations=[TokenAssociation{tokenId=5.4.3, accountId=8.7.6}], aliasKey=3036301006072a8648ce3d020106052b8104000a03220002703a9370b0443be6ae7c507b0aec81a55e94e4a863b9655360bd65358caa6588, children=[], duplicates=[], parentConsensusTimestamp=2019-04-01T22:42:22Z, ethereumHash=536f6d652068617368, paidStakingRewards=[Transfer{accountId=1.2.3, amount=8 ℏ}], prngBytes=766572792072616e646f6d206279746573, prngNumber=null, evmAddress=30783030, pendingAirdropRecords=[PendingAirdropRecord{pendingAirdropId=PendingAirdropId{sender=0.0.123, receiver=0.0.124, tokenId=null, nftId=0.0.5005/1234}, pendingAirdropAmount=123}, PendingAirdropRecord{pendingAirdropId=PendingAirdropId{sender=0.0.123, receiver=0.0.124, tokenId=0.0.12345, nftId=null}, pendingAirdropAmount=123}], highVolumePricingMultiplier=1000}"
]
\ No newline at end of file
diff --git a/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/RegisteredNodeAddressBookQueryIntegrationTest.java b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/RegisteredNodeAddressBookQueryIntegrationTest.java
new file mode 100644
index 000000000..bc45005da
--- /dev/null
+++ b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/RegisteredNodeAddressBookQueryIntegrationTest.java
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk.test.integration;
+
+import com.hedera.hashgraph.sdk.BlockNodeApi;
+import com.hedera.hashgraph.sdk.BlockNodeServiceEndpoint;
+import com.hedera.hashgraph.sdk.GeneralServiceEndpoint;
+import com.hedera.hashgraph.sdk.MirrorNodeServiceEndpoint;
+import com.hedera.hashgraph.sdk.PrivateKey;
+import com.hedera.hashgraph.sdk.RegisteredNodeAddressBookQuery;
+import com.hedera.hashgraph.sdk.RegisteredNodeCreateTransaction;
+import com.hedera.hashgraph.sdk.RegisteredServiceEndpoint;
+import com.hedera.hashgraph.sdk.RpcRelayServiceEndpoint;
+import java.util.List;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class RegisteredNodeAddressBookQueryIntegrationTest {
+ @Test
+ @DisplayName("Should query registered node using node id")
+ void canCreateAndVerifyRegisteredNodeWithPolling() throws Exception {
+ try (var testEnv = new IntegrationTestEnv(1)) {
+ var adminKey = PrivateKey.generateED25519();
+ var description = "Test Registered Node";
+
+ List serviceEndpoints = List.of(
+ new BlockNodeServiceEndpoint()
+ .setDomainName("test.block.com")
+ .setPort(443)
+ .addEndpointApi(BlockNodeApi.STATUS),
+ new MirrorNodeServiceEndpoint()
+ .setIpAddress(new byte[] {127, 0, 0, 1})
+ .setPort(443),
+ new RpcRelayServiceEndpoint().setDomainName("test.rpc.com").setPort(443),
+ new GeneralServiceEndpoint()
+ .setDomainName("test.general.com")
+ .setPort(8080));
+
+ System.out.println(testEnv.client);
+
+ var response = new RegisteredNodeCreateTransaction()
+ .setAdminKey(adminKey)
+ .setDescription(description)
+ .setServiceEndpoints(serviceEndpoints)
+ .freezeWith(testEnv.client)
+ .sign(adminKey)
+ .execute(testEnv.client);
+
+ var receipt = response.getReceipt(testEnv.client);
+ var nodeId = receipt.registeredNodeId;
+
+ // Wait for mirror node to update
+ Thread.sleep(5000);
+
+ var registeredNodeBook = new RegisteredNodeAddressBookQuery()
+ .setRegisteredNodeId(nodeId)
+ .execute(testEnv.client);
+
+ Assertions.assertThat(registeredNodeBook.registeredNodes).hasSize(1);
+
+ var node = registeredNodeBook.registeredNodes.get(0);
+
+ Assertions.assertThat(node.description).isEqualTo(description);
+ Assertions.assertThat(node.serviceEndpoints).hasSize(4);
+
+ Assertions.assertThat(node.serviceEndpoints)
+ .filteredOn(e -> e instanceof BlockNodeServiceEndpoint)
+ .first()
+ .extracting("domainName")
+ .isEqualTo("test.block.com");
+ }
+ }
+}
diff --git a/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/RegisteredNodeCreateTransactionIntegrationTest.java b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/RegisteredNodeCreateTransactionIntegrationTest.java
new file mode 100644
index 000000000..70d78cb5a
--- /dev/null
+++ b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/RegisteredNodeCreateTransactionIntegrationTest.java
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk.test.integration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.hedera.hashgraph.sdk.BlockNodeApi;
+import com.hedera.hashgraph.sdk.BlockNodeServiceEndpoint;
+import com.hedera.hashgraph.sdk.GeneralServiceEndpoint;
+import com.hedera.hashgraph.sdk.MirrorNodeServiceEndpoint;
+import com.hedera.hashgraph.sdk.PrivateKey;
+import com.hedera.hashgraph.sdk.RegisteredNodeCreateTransaction;
+import com.hedera.hashgraph.sdk.RegisteredServiceEndpoint;
+import com.hedera.hashgraph.sdk.RpcRelayServiceEndpoint;
+import com.hedera.hashgraph.sdk.Status;
+import java.util.List;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class RegisteredNodeCreateTransactionIntegrationTest {
+ @Test
+ @DisplayName("Can create a registered node with blockNodeServiceEndpoint")
+ void canCreateRegisteredNodeWithBlockNode() throws Exception {
+ try (var testEnv = new IntegrationTestEnv(1)) {
+ var key = PrivateKey.generateED25519();
+ List serviceEndpoints = List.of(new BlockNodeServiceEndpoint()
+ .setDomainName("test.block.com")
+ .setPort(443)
+ .addEndpointApi(BlockNodeApi.STATUS));
+
+ var response = new RegisteredNodeCreateTransaction()
+ .setAdminKey(key)
+ .setDescription("test description")
+ .setServiceEndpoints(serviceEndpoints)
+ .freezeWith(testEnv.client)
+ .sign(key)
+ .execute(testEnv.client);
+
+ var receipt = response.getReceipt(testEnv.client);
+
+ assertThat(receipt.status).isEqualTo(Status.SUCCESS);
+ assertThat(receipt.registeredNodeId).isGreaterThan(0);
+ }
+ }
+
+ @Test
+ @DisplayName("Can create a registered node with mirrorNodeServiceEndpoint")
+ void canCreateRegisteredNodeWitMirrorNode() throws Exception {
+ try (var testEnv = new IntegrationTestEnv(1)) {
+ var key = PrivateKey.generateED25519();
+ List serviceEndpoints = List.of(new MirrorNodeServiceEndpoint()
+ .setDomainName("test.mirror.com")
+ .setPort(443));
+
+ var response = new RegisteredNodeCreateTransaction()
+ .setAdminKey(key)
+ .setDescription("test description")
+ .setServiceEndpoints(serviceEndpoints)
+ .freezeWith(testEnv.client)
+ .sign(key)
+ .execute(testEnv.client);
+
+ var receipt = response.getReceipt(testEnv.client);
+
+ assertThat(receipt.status).isEqualTo(Status.SUCCESS);
+ assertThat(receipt.registeredNodeId).isGreaterThan(0);
+ }
+ }
+
+ @Test
+ @DisplayName("Can create a registered node with rpcRelayServiceEndpoint")
+ void canCreateRegisteredNodeWithRpcRelay() throws Exception {
+ try (var testEnv = new IntegrationTestEnv(1)) {
+ var key = PrivateKey.generateED25519();
+ List serviceEndpoints = List.of(
+ new RpcRelayServiceEndpoint().setDomainName("test.rpc.com").setPort(443));
+
+ var response = new RegisteredNodeCreateTransaction()
+ .setAdminKey(key)
+ .setDescription("test description")
+ .setServiceEndpoints(serviceEndpoints)
+ .freezeWith(testEnv.client)
+ .sign(key)
+ .execute(testEnv.client);
+
+ var receipt = response.getReceipt(testEnv.client);
+
+ assertThat(receipt.status).isEqualTo(Status.SUCCESS);
+ assertThat(receipt.registeredNodeId).isGreaterThan(0);
+ }
+ }
+
+ @Test
+ @DisplayName("Can create a registered node with generalServiceEndpoint")
+ void canCreateRegisteredNodeWithGeneralService() throws Exception {
+ try (var testEnv = new IntegrationTestEnv(1)) {
+ var key = PrivateKey.generateED25519();
+ List serviceEndpoints = List.of(new GeneralServiceEndpoint()
+ .setDomainName("test.general.com")
+ .setDescription("GeneralEndpoint")
+ .setPort(443));
+
+ var response = new RegisteredNodeCreateTransaction()
+ .setAdminKey(key)
+ .setDescription("test description")
+ .setServiceEndpoints(serviceEndpoints)
+ .freezeWith(testEnv.client)
+ .sign(key)
+ .execute(testEnv.client);
+
+ var receipt = response.getReceipt(testEnv.client);
+
+ assertThat(receipt.status).isEqualTo(Status.SUCCESS);
+ assertThat(receipt.registeredNodeId).isGreaterThan(0);
+ }
+ }
+
+ @Test
+ @DisplayName("Can create a registered node with multiple service endpoints")
+ void canCreateRegisteredNodeWithMultipleServiceEndpoints() throws Exception {
+ try (var testEnv = new IntegrationTestEnv(1)) {
+ var key = PrivateKey.generateED25519();
+ List serviceEndpoints = List.of(
+ new BlockNodeServiceEndpoint()
+ .setDomainName("test.block.com")
+ .setPort(443)
+ .addEndpointApi(BlockNodeApi.STATUS),
+ new MirrorNodeServiceEndpoint()
+ .setIpAddress(new byte[] {127, 0, 0, 1})
+ .setPort(443),
+ new RpcRelayServiceEndpoint().setDomainName("test.rpc.com").setPort(443),
+ new GeneralServiceEndpoint()
+ .setDomainName("test.general.com")
+ .setDescription("GeneralEndpoint")
+ .setPort(443));
+
+ var response = new RegisteredNodeCreateTransaction()
+ .setAdminKey(key)
+ .setDescription("test description")
+ .setServiceEndpoints(serviceEndpoints)
+ .freezeWith(testEnv.client)
+ .sign(key)
+ .execute(testEnv.client);
+
+ var receipt = response.getReceipt(testEnv.client);
+
+ assertThat(receipt.status).isEqualTo(Status.SUCCESS);
+ assertThat(receipt.registeredNodeId).isGreaterThan(0);
+ }
+ }
+}
diff --git a/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/RegisteredNodeDeleteTransactionIntegrationTest.java b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/RegisteredNodeDeleteTransactionIntegrationTest.java
new file mode 100644
index 000000000..fbce06977
--- /dev/null
+++ b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/RegisteredNodeDeleteTransactionIntegrationTest.java
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk.test.integration;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+
+import com.hedera.hashgraph.sdk.BlockNodeApi;
+import com.hedera.hashgraph.sdk.BlockNodeServiceEndpoint;
+import com.hedera.hashgraph.sdk.NodeUpdateTransaction;
+import com.hedera.hashgraph.sdk.PrivateKey;
+import com.hedera.hashgraph.sdk.ReceiptStatusException;
+import com.hedera.hashgraph.sdk.RegisteredNodeCreateTransaction;
+import com.hedera.hashgraph.sdk.RegisteredNodeDeleteTransaction;
+import com.hedera.hashgraph.sdk.RegisteredServiceEndpoint;
+import com.hedera.hashgraph.sdk.Status;
+import java.util.List;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class RegisteredNodeDeleteTransactionIntegrationTest {
+ @Test
+ @DisplayName("Can delete a registered node")
+ void canDeleteRegisteredNode() throws Exception {
+ try (var testEnv = new IntegrationTestEnv(1)) {
+ var key = PrivateKey.generateED25519();
+
+ List endpoints = List.of(new BlockNodeServiceEndpoint()
+ .setDomainName("blocks.example.com")
+ .setPort(443)
+ .addEndpointApi(BlockNodeApi.STATUS));
+
+ var createReceipt = new RegisteredNodeCreateTransaction()
+ .setAdminKey(key)
+ .setDescription("test node delete")
+ .setServiceEndpoints(endpoints)
+ .freezeWith(testEnv.client)
+ .sign(key)
+ .execute(testEnv.client)
+ .getReceipt(testEnv.client);
+
+ var nodeId = createReceipt.registeredNodeId;
+
+ var deleteReceipt = new RegisteredNodeDeleteTransaction()
+ .setRegisteredNodeId(nodeId)
+ .freezeWith(testEnv.client)
+ .sign(key)
+ .execute(testEnv.client)
+ .getReceipt(testEnv.client);
+
+ assertThat(deleteReceipt.status).isEqualTo(Status.SUCCESS);
+ }
+ }
+
+ @Test
+ @DisplayName("Should return REGISTERED_NODE_STILL_ASSOCIATED when deleting an associated node")
+ void shouldCauseReceiptStatusWhenNodeStillAssociated() throws Exception {
+ try (var testEnv = new IntegrationTestEnv(1)) {
+ var key = PrivateKey.generateED25519();
+
+ List endpoints = List.of(new BlockNodeServiceEndpoint()
+ .setDomainName("blocks.example.com")
+ .setPort(443)
+ .addEndpointApi(BlockNodeApi.STATUS));
+
+ var createReceipt = new RegisteredNodeCreateTransaction()
+ .setAdminKey(key)
+ .setDescription("test node delete")
+ .setServiceEndpoints(endpoints)
+ .freezeWith(testEnv.client)
+ .sign(key)
+ .execute(testEnv.client)
+ .getReceipt(testEnv.client);
+
+ var registeredNodeId = createReceipt.registeredNodeId;
+
+ new NodeUpdateTransaction()
+ .setNodeId(0)
+ .addAssociatedRegisteredNode(registeredNodeId)
+ .freezeWith(testEnv.client)
+ .execute(testEnv.client)
+ .getReceipt(testEnv.client);
+
+ var deleteTx = new RegisteredNodeDeleteTransaction()
+ .setRegisteredNodeId(registeredNodeId)
+ .freezeWith(testEnv.client)
+ .sign(key)
+ .execute(testEnv.client);
+
+ assertThatThrownBy(() -> deleteTx.getReceipt(testEnv.client))
+ .isInstanceOf(ReceiptStatusException.class)
+ .satisfies(e -> {
+ assertThat(((ReceiptStatusException) e).receipt.status)
+ .isEqualTo(Status.REGISTERED_NODE_STILL_ASSOCIATED);
+ });
+ }
+ }
+}
diff --git a/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/RegisteredNodeUpdateTransactionIntegrationTest.java b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/RegisteredNodeUpdateTransactionIntegrationTest.java
new file mode 100644
index 000000000..69c5291e0
--- /dev/null
+++ b/sdk/src/testIntegration/java/com/hedera/hashgraph/sdk/test/integration/RegisteredNodeUpdateTransactionIntegrationTest.java
@@ -0,0 +1,96 @@
+// SPDX-License-Identifier: Apache-2.0
+package com.hedera.hashgraph.sdk.test.integration;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+import com.hedera.hashgraph.sdk.BlockNodeApi;
+import com.hedera.hashgraph.sdk.BlockNodeServiceEndpoint;
+import com.hedera.hashgraph.sdk.PrivateKey;
+import com.hedera.hashgraph.sdk.RegisteredNodeCreateTransaction;
+import com.hedera.hashgraph.sdk.RegisteredNodeUpdateTransaction;
+import com.hedera.hashgraph.sdk.RegisteredServiceEndpoint;
+import com.hedera.hashgraph.sdk.Status;
+import java.util.List;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class RegisteredNodeUpdateTransactionIntegrationTest {
+ @Test
+ @DisplayName("Can update registered node endpoints and description")
+ void canUpdateRegisteredNode() throws Exception {
+ try (var testEnv = new IntegrationTestEnv(1)) {
+ var key = PrivateKey.generateED25519();
+
+ List endpoints = List.of(new BlockNodeServiceEndpoint()
+ .setDomainName("initial.blocks.com")
+ .setPort(443)
+ .addEndpointApi(BlockNodeApi.SUBSCRIBE_STREAM));
+
+ var createResponse = new RegisteredNodeCreateTransaction()
+ .setAdminKey(key)
+ .setDescription("test initial node")
+ .setServiceEndpoints(endpoints)
+ .freezeWith(testEnv.client)
+ .sign(key)
+ .execute(testEnv.client);
+
+ var createReceipt = createResponse.getReceipt(testEnv.client);
+
+ var nodeId = createReceipt.registeredNodeId;
+
+ List updatedEndpoints = List.of(new BlockNodeServiceEndpoint()
+ .setDomainName("updated.blocks.com")
+ .setPort(443)
+ .addEndpointApi(BlockNodeApi.STATUS));
+
+ var updateResponse = new RegisteredNodeUpdateTransaction()
+ .setRegisteredNodeId(nodeId)
+ .setDescription("test updated node")
+ .setServiceEndpoints(updatedEndpoints)
+ .freezeWith(testEnv.client)
+ .sign(key)
+ .execute(testEnv.client);
+
+ var updateReceipt = updateResponse.getReceipt(testEnv.client);
+
+ assertThat(updateReceipt.status).isEqualTo(Status.SUCCESS);
+ }
+ }
+
+ @Test
+ @DisplayName("Can rotate registered node admin key")
+ void canRotateAdminKey() throws Exception {
+ try (var testEnv = new IntegrationTestEnv(1)) {
+ var oldKey = PrivateKey.generateED25519();
+
+ List endpoints = List.of(new BlockNodeServiceEndpoint()
+ .setDomainName("blocks.example.com")
+ .setPort(443)
+ .addEndpointApi(BlockNodeApi.STATUS));
+
+ var createReceipt = new RegisteredNodeCreateTransaction()
+ .setAdminKey(oldKey)
+ .setDescription("test node")
+ .setServiceEndpoints(endpoints)
+ .freezeWith(testEnv.client)
+ .sign(oldKey)
+ .execute(testEnv.client)
+ .getReceipt(testEnv.client);
+
+ var nodeId = createReceipt.registeredNodeId;
+
+ var newKey = PrivateKey.generateED25519();
+
+ var tx = new RegisteredNodeUpdateTransaction()
+ .setRegisteredNodeId(nodeId)
+ .setAdminKey(newKey)
+ .freezeWith(testEnv.client);
+
+ tx.sign(oldKey);
+ tx.sign(newKey);
+
+ var receipt = tx.execute(testEnv.client).getReceipt(testEnv.client);
+ assertThat(receipt.status).isEqualTo(Status.SUCCESS);
+ }
+ }
+}