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); + } + } +}