From 0adfe0ea7a3ff87066cb16cfcfa4015b8d83f950 Mon Sep 17 00:00:00 2001 From: Nikita Lebedev Date: Thu, 22 Aug 2024 12:20:10 +0300 Subject: [PATCH] feat(HIP-869): Dynamic Address Book - Stage 1 - HAPI Endpoints (#1892) Signed-off-by: Nikita Lebedev Signed-off-by: Ivan Ivanov Co-authored-by: Ivan Ivanov --- .../examples/DynamicAddressBookExample.java | 97 ++++ .../com/hedera/hashgraph/sdk/Endpoint.java | 38 +- .../com/hedera/hashgraph/sdk/IPv4Address.java | 2 +- .../hedera/hashgraph/sdk/IPv4AddressPart.java | 2 +- .../hashgraph/sdk/NodeCreateTransaction.java | 439 +++++++++++++++ .../hashgraph/sdk/NodeDeleteTransaction.java | 145 +++++ .../hashgraph/sdk/NodeUpdateTransaction.java | 502 ++++++++++++++++++ .../com/hedera/hashgraph/sdk/Transaction.java | 6 + .../hashgraph/sdk/TransactionReceipt.java | 18 + .../sdk/AddressBookQueryMockTest.java | 4 +- .../sdk/NodeCreateTransactionTest.java | 250 +++++++++ .../sdk/NodeCreateTransactionTest.snap | 3 + .../sdk/NodeDeleteTransactionTest.java | 110 ++++ .../sdk/NodeDeleteTransactionTest.snap | 3 + .../sdk/NodeUpdateTransactionTest.java | 270 ++++++++++ .../sdk/NodeUpdateTransactionTest.snap | 3 + .../hashgraph/sdk/TransactionReceiptTest.java | 1 + .../hashgraph/sdk/TransactionReceiptTest.snap | 4 +- .../hashgraph/sdk/TransactionRecordTest.snap | 4 +- 19 files changed, 1889 insertions(+), 12 deletions(-) create mode 100644 examples/src/main/java/com/hedera/hashgraph/sdk/examples/DynamicAddressBookExample.java create mode 100644 sdk/src/main/java/com/hedera/hashgraph/sdk/NodeCreateTransaction.java create mode 100644 sdk/src/main/java/com/hedera/hashgraph/sdk/NodeDeleteTransaction.java create mode 100644 sdk/src/main/java/com/hedera/hashgraph/sdk/NodeUpdateTransaction.java create mode 100644 sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.java create mode 100644 sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.snap create mode 100644 sdk/src/test/java/com/hedera/hashgraph/sdk/NodeDeleteTransactionTest.java create mode 100644 sdk/src/test/java/com/hedera/hashgraph/sdk/NodeDeleteTransactionTest.snap create mode 100644 sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.java create mode 100644 sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.snap diff --git a/examples/src/main/java/com/hedera/hashgraph/sdk/examples/DynamicAddressBookExample.java b/examples/src/main/java/com/hedera/hashgraph/sdk/examples/DynamicAddressBookExample.java new file mode 100644 index 0000000000..18262a281c --- /dev/null +++ b/examples/src/main/java/com/hedera/hashgraph/sdk/examples/DynamicAddressBookExample.java @@ -0,0 +1,97 @@ +package com.hedera.hashgraph.sdk.examples; + +import com.hedera.hashgraph.sdk.AccountId; +import com.hedera.hashgraph.sdk.Client; +import com.hedera.hashgraph.sdk.Endpoint; +import com.hedera.hashgraph.sdk.IPv4Address; +import com.hedera.hashgraph.sdk.IPv4AddressPart; +import com.hedera.hashgraph.sdk.NodeCreateTransaction; +import com.hedera.hashgraph.sdk.NodeDeleteTransaction; +import com.hedera.hashgraph.sdk.NodeUpdateTransaction; +import com.hedera.hashgraph.sdk.PrecheckStatusException; +import com.hedera.hashgraph.sdk.PrivateKey; +import com.hedera.hashgraph.sdk.ReceiptStatusException; +import io.github.cdimascio.dotenv.Dotenv; +import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.TimeoutException; + +/** + * hip-869 + */ +public class DynamicAddressBookExample { + // see `.env.sample` in the repository root for how to specify these values + // or set environment variables with the same names + private static final AccountId OPERATOR_ID = AccountId.fromString( + Objects.requireNonNull(Dotenv.load().get("OPERATOR_ID"))); + 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 + private static final String HEDERA_NETWORK = Dotenv.load().get("HEDERA_NETWORK", "testnet"); + + public static void main(String[] args) + throws TimeoutException, PrecheckStatusException, ReceiptStatusException, InterruptedException { + Client client = ClientHelper.forName(HEDERA_NETWORK); + + // Defaults the operator account ID and key such that all generated transactions will be paid for + // by this account and be signed by this key + client.setOperator(OPERATOR_ID, OPERATOR_KEY); + + AccountId accountId = AccountId.fromString("0.0.1999"); + String description = "Hedera™ cryptocurrency"; + String newDescription = "Hedera™ cryptocurrency - updated"; + + // Set up IPv4 address + IPv4Address ipv4Address = new IPv4Address(); + ipv4Address.setHost(new IPv4AddressPart()); + ipv4Address.setNetwork(new IPv4AddressPart()); + Endpoint gossipEndpoint = new Endpoint(); + gossipEndpoint.setAddress(ipv4Address); + + // Set up service endpoint + Endpoint serviceEndpoint = new Endpoint(); + serviceEndpoint.setAddress(ipv4Address); + + // Generate admin key + PrivateKey adminKey = PrivateKey.generateED25519(); + + // Create node create transaction + NodeCreateTransaction nodeCreateTransaction = new NodeCreateTransaction() + .setAccountId(accountId) + .setDescription(description) + .setGossipCaCertificate("gossipCaCertificate".getBytes()) + .setServiceEndpoints(Collections.singletonList(serviceEndpoint)) + .setGossipEndpoints(Collections.singletonList(gossipEndpoint)) + .setAdminKey(adminKey.getPublicKey()); + + try { + nodeCreateTransaction.execute(client).getReceipt(client); + } catch (Exception e){ + System.out.println(e); + } + + var nodeUpdateTransaction = new NodeUpdateTransaction() + .setNodeId(123) + .setAccountId(accountId) + .setDescription(newDescription) + .setGossipCaCertificate("gossipCaCertificate".getBytes()) + .setServiceEndpoints(Collections.singletonList(serviceEndpoint)) + .setGossipEndpoints(Collections.singletonList(gossipEndpoint)) + .setAdminKey(adminKey.getPublicKey()); + + try { + nodeUpdateTransaction.execute(client).getReceipt(client); + } catch (Exception e){ + System.out.println(e); + } + + var nodeDeleteTransaction = new NodeDeleteTransaction() + .setNodeId(123); + + try { + nodeDeleteTransaction.execute(client).getReceipt(client); + } catch (Exception e){ + System.out.println(e); + } + } +} diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/Endpoint.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/Endpoint.java index 095c5e7a11..dd3b48f636 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/Endpoint.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/Endpoint.java @@ -29,15 +29,18 @@ * Utility class used internally by the sdk. */ public class Endpoint implements Cloneable { + @Nullable IPv4Address address = null; int port; + String domainName = ""; + /** * Constructor. */ - Endpoint() { + public Endpoint() { } /** @@ -55,7 +58,8 @@ static Endpoint fromProtobuf(ServiceEndpoint serviceEndpoint) { return new Endpoint() .setAddress(IPv4Address.fromProtobuf(serviceEndpoint.getIpAddressV4())) - .setPort(port); + .setPort(port) + .setDomainName(serviceEndpoint.getDomainName()); } /** @@ -99,6 +103,26 @@ public Endpoint setPort(int port) { return this; } + /** + * Extract the domain name. + * + * @return the domain name + */ + public String getDomainName() { + return domainName; + } + + /** + * Assign the desired domain name. + * + * @param domainName the desired domain name + * @return {@code this} + */ + public Endpoint setDomainName(String domainName) { + this.domainName = domainName; + return this; + } + /** * Create the protobuf. * @@ -111,14 +135,18 @@ ServiceEndpoint toProtobuf() { builder.setIpAddressV4(address.toProtobuf()); } + builder.setDomainName(domainName); + return builder.setPort(port).build(); } @Override public String toString() { - return Objects.requireNonNull(address) + - ":" + - port; + if (this.domainName != null && !this.domainName.isEmpty()) { + return domainName + ":" + port; + } else { + return Objects.requireNonNull(address) + ":" + port; + } } @Override diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/IPv4Address.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/IPv4Address.java index 5e1e908b43..e8f5c5eca1 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/IPv4Address.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/IPv4Address.java @@ -42,7 +42,7 @@ public class IPv4Address implements Cloneable { /** * Constructor. */ - IPv4Address() { + public IPv4Address() { } /** diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/IPv4AddressPart.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/IPv4AddressPart.java index eb834707e6..ae37960a8a 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/IPv4AddressPart.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/IPv4AddressPart.java @@ -35,7 +35,7 @@ public class IPv4AddressPart implements Cloneable { /** * Constructor. */ - IPv4AddressPart() { + public IPv4AddressPart() { } /** diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeCreateTransaction.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeCreateTransaction.java new file mode 100644 index 0000000000..3f3be13b02 --- /dev/null +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeCreateTransaction.java @@ -0,0 +1,439 @@ +/*- + * + * Hedera Java SDK + * + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.hedera.hashgraph.sdk; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.hedera.hashgraph.sdk.proto.AddressBookServiceGrpc; +import com.hedera.hashgraph.sdk.proto.NodeCreateTransactionBody; +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.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nullable; +import org.bouncycastle.util.Arrays; + +/** + * A transaction to create a new node in the network address book. + * The transaction, once complete, enables a new consensus node + * to join the network, and requires governing council authorization. + *

+ * This transaction body SHALL be considered a "privileged transaction". + *

+ * + * - MUST be signed by the governing council. + * - MUST be signed by the `Key` assigned to the + * `admin_key` field. + * - The newly created node information SHALL be added to the network address + * book information in the network state. + * - The new entry SHALL be created in "state" but SHALL NOT participate in + * network consensus and SHALL NOT be present in network "configuration" + * until the next "upgrade" transaction (as noted below). + * - All new address book entries SHALL be added to the active network + * configuration during the next `freeze` transaction with the field + * `freeze_type` set to `PREPARE_UPGRADE`. + * + * ### Record Stream Effects + * Upon completion the newly assigned `node_id` SHALL be in the transaction + * receipt. + */ +public class NodeCreateTransaction extends Transaction { + + /** + * A Node account identifier. + *

+ * This account identifier MUST be in the "account number" form.
+ * This account identifier MUST NOT use the alias field.
+ * If the identified account does not exist, this transaction SHALL fail.
+ * Multiple nodes MAY share the same node account.
+ * This field is REQUIRED. + */ + @Nullable + private AccountId accountId = null; + + /** + * A short description of the node. + *

+ * This value, if set, MUST NOT exceed 100 bytes when encoded as UTF-8.
+ * This field is OPTIONAL. + */ + private String description = ""; + + /** + * A list of service endpoints for gossip. + *

+ * These endpoints SHALL represent the published endpoints to which other + * consensus nodes may _gossip_ transactions.
+ * These endpoints MUST specify a port.
+ * This list MUST NOT be empty.
+ * This list MUST NOT contain more than `10` entries.
+ * The first two entries in this list SHALL be the endpoints published to + * all consensus nodes.
+ * All other entries SHALL be reserved for future use. + *

+ * Each network may have additional requirements for these endpoints. + * A client MUST check network-specific documentation for those + * details.
+ * If the network configuration value `gossipFqdnRestricted` is set, then + * all endpoints in this list MUST supply only IP address.
+ * If the network configuration value `gossipFqdnRestricted` is _not_ set, + * then endpoints in this list MAY supply either IP address or FQDN, but + * MUST NOT supply both values for the same endpoint. + */ + private List gossipEndpoints = new ArrayList<>(); + + /** + * A list of service endpoints for gRPC calls. + *

+ * These endpoints SHALL represent the published gRPC endpoints to which + * clients may submit transactions.
+ * These endpoints MUST specify a port.
+ * Endpoints in this list MAY supply either IP address or FQDN, but MUST + * NOT supply both values for the same endpoint.
+ * This list MUST NOT be empty.
+ * This list MUST NOT contain more than `8` entries. + */ + private List serviceEndpoints = new ArrayList<>(); + + /** + * A certificate used to sign gossip events. + *

+ * This value MUST be a certificate of a type permitted for gossip + * signatures.
+ * This value MUST be the DER encoding of the certificate presented.
+ * This field is REQUIRED and MUST NOT be empty. + */ + @Nullable + private byte[] gossipCaCertificate = null; + + /** + * A hash of the node gRPC TLS certificate. + *

+ * This value MAY be used to verify the certificate presented by the node + * during TLS negotiation for gRPC.
+ * This value MUST be a SHA-384 hash.
+ * The TLS certificate to be hashed MUST first be in PEM format and MUST be + * encoded with UTF-8 NFKD encoding to a stream of bytes provided to + * the hash algorithm.
+ * This field is OPTIONAL. + */ + @Nullable + private byte[] grpcCertificateHash = null; + + /** + * An 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 and MUST NOT be set to an empty `KeyList`. + */ + @Nullable + private Key adminKey = null; + + /** + * Constructor. + */ + public NodeCreateTransaction() {} + + /** + * Constructor. + * + * @param txs Compound list of transaction id's list of (AccountId, Transaction) records + * @throws InvalidProtocolBufferException when there is an issue with the protobuf + */ + NodeCreateTransaction( + LinkedHashMap> txs) + throws InvalidProtocolBufferException { + super(txs); + initFromTransactionBody(); + } + + /** + * Constructor. + * + * @param txBody protobuf TransactionBody + */ + NodeCreateTransaction(com.hedera.hashgraph.sdk.proto.TransactionBody txBody) { + super(txBody); + initFromTransactionBody(); + } + + /** + * Extract the Account ID of the Node. + * @return the Account ID of the Node. + */ + public AccountId getAccountId() { + return accountId; + } + + /** + * Assign the Account ID of the Node. + * @param accountId the Account ID of the Node. + * @return {@code this} + */ + public NodeCreateTransaction setAccountId(AccountId accountId) { + Objects.requireNonNull(accountId); + requireNotFrozen(); + this.accountId = accountId; + return this; + } + + /** + * Extract the description of the node. + * @return the node's description. + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of the node. + * @param description The String to be set as the description of the node. + * @return {@code this} + */ + public NodeCreateTransaction setDescription(String description) { + requireNotFrozen(); + Objects.requireNonNull(description); + this.description = description; + return this; + } + + /** + * Extract the list of service endpoints for gossip. + * @return the list of service endpoints for gossip. + */ + public List getGossipEndpoints() { + return gossipEndpoints; + } + + /** + * Assign the list of service endpoints for gossip. + * @param gossipEndpoints the list of service endpoints for gossip. + * @return {@code this} + */ + public NodeCreateTransaction setGossipEndpoints(List gossipEndpoints) { + requireNotFrozen(); + Objects.requireNonNull(gossipEndpoints); + this.gossipEndpoints = new ArrayList<>(gossipEndpoints); + return this; + } + + /** + * Add an endpoint for gossip to the list of service endpoints for gossip. + * @param gossipEndpoint endpoints for gossip to add. + * @return {@code this} + */ + public NodeCreateTransaction addGossipEndpoint(Endpoint gossipEndpoint) { + requireNotFrozen(); + gossipEndpoints.add(gossipEndpoint); + return this; + } + + /** + * Extract the list of service endpoints for gRPC calls. + * @return the list of service endpoints for gRPC calls. + */ + public List getServiceEndpoints() { + return serviceEndpoints; + } + + /** + * Assign the list of service endpoints for gRPC calls. + * @param serviceEndpoints list of service endpoints for gRPC calls. + * @return {@code this} + */ + public NodeCreateTransaction setServiceEndpoints(List serviceEndpoints) { + requireNotFrozen(); + Objects.requireNonNull(serviceEndpoints); + this.serviceEndpoints = new ArrayList<>(serviceEndpoints); + return this; + } + + /** + * Add an endpoint for gRPC calls to the list of service endpoints for gRPC calls. + * @param serviceEndpoint endpoints for gRPC calls to add. + * @return {@code this} + */ + public NodeCreateTransaction addServiceEndpoint(Endpoint serviceEndpoint) { + requireNotFrozen(); + serviceEndpoints.add(serviceEndpoint); + return this; + } + + /** + * Extract the certificate used to sign gossip events. + * @return the DER encoding of the certificate presented. + */ + @Nullable + public byte[] getGossipCaCertificate() { + return gossipCaCertificate != null ? Arrays.copyOf(gossipCaCertificate, gossipCaCertificate.length) : null; + } + + /** + * Sets the certificate used to sign gossip events. + *
+ * This value MUST be the DER encoding of the certificate presented. + * @param gossipCaCertificate the DER encoding of the certificate presented. + * @return {@code this} + */ + public NodeCreateTransaction setGossipCaCertificate(byte[] gossipCaCertificate) { + Objects.requireNonNull(gossipCaCertificate); + requireNotFrozen(); + this.gossipCaCertificate = Arrays.copyOf(gossipCaCertificate, gossipCaCertificate.length); + return this; + } + + /** + * Extract the hash of the node gRPC TLS certificate. + * @return SHA-384 hash of the node gRPC TLS certificate. + */ + @Nullable + public byte[] getGrpcCertificateHash() { + return grpcCertificateHash != null ? Arrays.copyOf(grpcCertificateHash, grpcCertificateHash.length) : null; + } + + /** + * Sets the hash of the node gRPC TLS certificate. + *
+ * This value MUST be a SHA-384 hash. + * @param grpcCertificateHash SHA-384 hash of the node gRPC TLS certificate. + * @return {@code this} + */ + public NodeCreateTransaction setGrpcCertificateHash(byte[] grpcCertificateHash) { + Objects.requireNonNull(grpcCertificateHash); + requireNotFrozen(); + this.grpcCertificateHash = Arrays.copyOf(grpcCertificateHash, grpcCertificateHash.length); + return this; + } + + /** + * Get an administrative key controlled by the node operator. + * @return an administrative key controlled by the node operator. + */ + @Nullable + public Key getAdminKey() { + return adminKey; + } + + /** + * Sets an administrative key controlled by the node operator. + * @param adminKey an administrative key to be set. + * @return {@code this} + */ + public NodeCreateTransaction setAdminKey(Key adminKey) { + Objects.requireNonNull(adminKey); + requireNotFrozen(); + this.adminKey = adminKey; + return this; + } + + /** + * Build the transaction body. + * + * @return {@link com.hedera.hashgraph.sdk.proto.NodeCreateTransactionBody} + */ + NodeCreateTransactionBody.Builder build() { + var builder = NodeCreateTransactionBody.newBuilder(); + + if (accountId != null) { + builder.setAccountId(accountId.toProtobuf()); + } + + builder.setDescription(description); + + for (Endpoint gossipEndpoint : gossipEndpoints) { + builder.addGossipEndpoint(gossipEndpoint.toProtobuf()); + } + + for (Endpoint serviceEndpoint : serviceEndpoints) { + builder.addServiceEndpoint(serviceEndpoint.toProtobuf()); + } + + if (gossipCaCertificate != null) { + builder.setGossipCaCertificate(ByteString.copyFrom(gossipCaCertificate)); + } + + if (grpcCertificateHash != null) { + builder.setGrpcCertificateHash(ByteString.copyFrom(grpcCertificateHash)); + } + + if (adminKey != null) { + builder.setAdminKey(adminKey.toProtobufKey()); + } + + return builder; + } + + /** + * Initialize from the transaction body. + */ + void initFromTransactionBody() { + var body = sourceTransactionBody.getNodeCreate(); + + if (body.hasAccountId()) { + accountId = AccountId.fromProtobuf(body.getAccountId()); + } + + description = body.getDescription(); + + for (var gossipEndpoint : body.getGossipEndpointList()) { + gossipEndpoints.add(Endpoint.fromProtobuf(gossipEndpoint)); + } + + for (var serviceEndpoint : body.getServiceEndpointList()) { + serviceEndpoints.add(Endpoint.fromProtobuf(serviceEndpoint)); + } + + gossipCaCertificate = body.getGossipCaCertificate().toByteArray(); + + grpcCertificateHash = body.getGrpcCertificateHash().toByteArray(); + + if (body.hasAdminKey()) { + adminKey = Key.fromProtobufKey(body.getAdminKey()); + } + } + + @Override + void validateChecksums(Client client) throws BadEntityIdException { + if (accountId != null) { + accountId.validateChecksum(client); + } + } + + @Override + MethodDescriptor getMethodDescriptor() { + return AddressBookServiceGrpc.getCreateNodeMethod(); + } + + @Override + void onFreeze(TransactionBody.Builder bodyBuilder) { + bodyBuilder.setNodeCreate(build()); + } + + @Override + void onScheduled(SchedulableTransactionBody.Builder scheduled) { + scheduled.setNodeCreate(build()); + } +} diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeDeleteTransaction.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeDeleteTransaction.java new file mode 100644 index 0000000000..37c594f3c2 --- /dev/null +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeDeleteTransaction.java @@ -0,0 +1,145 @@ +/*- + * + * Hedera Java SDK + * + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.hedera.hashgraph.sdk; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.hedera.hashgraph.sdk.proto.AddressBookServiceGrpc; +import com.hedera.hashgraph.sdk.proto.NodeDeleteTransactionBody; +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; + +/** + * A transaction to delete a node from the network address book. + * + * This transaction body SHALL be considered a "privileged transaction". + * + * - A transaction MUST be signed by the governing council. + * - Upon success, the address book entry SHALL enter a "pending delete" + * state. + * - All address book entries pending deletion SHALL be removed from the + * active network configuration during the next `freeze` transaction with + * the field `freeze_type` set to `PREPARE_UPGRADE`.
+ * - A deleted address book node SHALL be removed entirely from network state. + * - A deleted address book node identifier SHALL NOT be reused. + * + * ### Record Stream Effects + * Upon completion the "deleted" `node_id` SHALL be in the transaction + * receipt. + */ +public class NodeDeleteTransaction extends Transaction { + + /** + * A consensus node identifier in the network state. + *

+ * The node identified MUST exist in the network address book.
+ * The node identified MUST NOT be deleted.
+ * This value is REQUIRED. + */ + private long nodeId = 0; + + /** + * Constructor. + */ + public NodeDeleteTransaction() {} + + /** + * Constructor. + * + * @param txs Compound list of transaction id's list of (AccountId, Transaction) records + * @throws InvalidProtocolBufferException when there is an issue with the protobuf + */ + NodeDeleteTransaction( + LinkedHashMap> txs) + throws InvalidProtocolBufferException { + super(txs); + initFromTransactionBody(); + } + + /** + * Constructor. + * + * @param txBody protobuf TransactionBody + */ + NodeDeleteTransaction(TransactionBody txBody) { + super(txBody); + initFromTransactionBody(); + } + + /** + * Extract the consensus node identifier in the network state. + * @return the consensus node identifier in the network state. + */ + public long getNodeId() { + return nodeId; + } + + /** + * Assign the consensus node identifier in the network state. + * @param nodeId the consensus node identifier in the network state. + * @return {@code this} + */ + public NodeDeleteTransaction setNodeId(long nodeId) { + requireNotFrozen(); + this.nodeId = nodeId; + return this; + } + + /** + * Build the transaction body. + * + * @return {@link com.hedera.hashgraph.sdk.proto.NodeDeleteTransactionBody} + */ + NodeDeleteTransactionBody.Builder build() { + var builder = NodeDeleteTransactionBody.newBuilder(); + builder.setNodeId(nodeId); + return builder; + } + + /** + * Initialize from the transaction body. + */ + void initFromTransactionBody() { + var body = sourceTransactionBody.getNodeDelete(); + nodeId = body.getNodeId(); + } + + @Override + void validateChecksums(Client client) throws BadEntityIdException { + // no-op + } + + @Override + MethodDescriptor getMethodDescriptor() { + return AddressBookServiceGrpc.getDeleteNodeMethod(); + } + + @Override + void onFreeze(TransactionBody.Builder bodyBuilder) { + bodyBuilder.setNodeDelete(build()); + } + + @Override + void onScheduled(SchedulableTransactionBody.Builder scheduled) { + scheduled.setNodeDelete(build()); + } +} diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeUpdateTransaction.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeUpdateTransaction.java new file mode 100644 index 0000000000..7875b24106 --- /dev/null +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/NodeUpdateTransaction.java @@ -0,0 +1,502 @@ +/*- + * + * Hedera Java SDK + * + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.hedera.hashgraph.sdk; + +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.StringValue; +import com.hedera.hashgraph.sdk.proto.AddressBookServiceGrpc; +import com.hedera.hashgraph.sdk.proto.NodeUpdateTransactionBody; +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.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nullable; +import org.bouncycastle.util.Arrays; + +/** + * A transaction to modify address book node attributes. + * + * - This transaction SHALL enable the node operator, as identified by the + * `admin_key`, to modify operational attributes of the node. + * - This transaction MUST be signed by the active `admin_key` for the node. + * - If this transaction sets a new value for the `admin_key`, then both the + * current `admin_key`, and the new `admin_key` MUST sign this transaction. + * - This transaction SHALL NOT change any field that is not set (is null) in + * this transaction body. + * - This SHALL create a pending update to the node, but the change SHALL NOT + * be immediately applied to the active configuration. + * - All pending node updates SHALL be applied to the active network + * configuration during the next `freeze` transaction with the field + * `freeze_type` set to `PREPARE_UPGRADE`. + * + * ### Record Stream Effects + * Upon completion the `node_id` for the updated entry SHALL be in the + * transaction receipt. + */ +public class NodeUpdateTransaction extends Transaction { + + /** + * A consensus node identifier in the network state. + *

+ * The node identified MUST exist in the network address book.
+ * The node identified MUST NOT be deleted.
+ * This value is REQUIRED. + */ + private long nodeId = 0; + + /** + * An account identifier. + *

+ * If set, this SHALL replace the node account identifier.
+ * If set, this transaction MUST be signed by the active `key` for _both_ + * the current node account _and_ the identified new node account. + */ + @Nullable + private AccountId accountId = null; + + /** + * A short description of the node. + *

+ * This value, if set, MUST NOT exceed 100 bytes when encoded as UTF-8.
+ * If set, this value SHALL replace the previous value. + */ + @Nullable + private String description = null; + + /** + * A list of service endpoints for gossip. + *

+ * If set, this list MUST meet the following requirements. + *


+ * These endpoints SHALL represent the published endpoints to which other + * consensus nodes may _gossip_ transactions.
+ * These endpoints SHOULD NOT specify both address and DNS name.
+ * This list MUST NOT be empty.
+ * This list MUST NOT contain more than `10` entries.
+ * The first two entries in this list SHALL be the endpoints published to + * all consensus nodes.
+ * All other entries SHALL be reserved for future use. + *

+ * Each network may have additional requirements for these endpoints. + * A client MUST check network-specific documentation for those + * details.
+ *

Example
+ * Hedera Mainnet _requires_ that address be specified, and does not + * permit DNS name (FQDN) to be specified.
+ * Mainnet also requires that the first entry be an "internal" IP + * address and the second entry be an "external" IP address. + *
+ *
+ * Solo, however, _requires_ DNS name (FQDN) but also permits + * address. + *
+ *

+ * If set, the new list SHALL replace the existing list. + */ + private List gossipEndpoints = new ArrayList<>(); + + /** + * A list of service endpoints for gRPC calls. + *

+ * If set, this list MUST meet the following requirements. + *


+ * These endpoints SHALL represent the published endpoints to which clients + * may submit transactions.
+ * These endpoints SHOULD specify address and port.
+ * These endpoints MAY specify a DNS name.
+ * These endpoints SHOULD NOT specify both address and DNS name.
+ * This list MUST NOT be empty.
+ * This list MUST NOT contain more than `8` entries. + *

+ * Each network may have additional requirements for these endpoints. + * A client MUST check network-specific documentation for those + * details. + *

+ * If set, the new list SHALL replace the existing list. + */ + private List serviceEndpoints = new ArrayList<>(); + + /** + * A certificate used to sign gossip events. + *

+ * This value MUST be a certificate of a type permitted for gossip + * signatures.
+ * This value MUST be the DER encoding of the certificate presented. + *

+ * If set, the new value SHALL replace the existing bytes value. + */ + @Nullable + private byte[] gossipCaCertificate = null; + + /** + * A hash of the node gRPC TLS certificate. + *

+ * This value MAY be used to verify the certificate presented by the node + * during TLS negotiation for gRPC.
+ * This value MUST be a SHA-384 hash.
+ * The TLS certificate to be hashed MUST first be in PEM format and MUST be + * encoded with UTF-8 NFKD encoding to a stream of bytes provided to + * the hash algorithm.
+ *

+ * If set, the new value SHALL replace the existing hash value. + */ + @Nullable + private byte[] grpcCertificateHash = null; + + /** + * An administrative key controlled by the node operator. + *

+ * This field is OPTIONAL.
+ * If set, this key MUST sign this transaction.
+ * If set, this key MUST sign each subsequent transaction to + * update this node.
+ * If set, this field MUST contain a valid `Key` value.
+ * If set, this field MUST NOT be set to an empty `KeyList`. + */ + @Nullable + private Key adminKey = null; + + /** + * Constructor. + */ + public NodeUpdateTransaction() {} + + /** + * Constructor. + * + * @param txs Compound list of transaction id's list of (AccountId, Transaction) records + * @throws InvalidProtocolBufferException when there is an issue with the protobuf + */ + NodeUpdateTransaction( + LinkedHashMap> txs) + throws InvalidProtocolBufferException { + super(txs); + initFromTransactionBody(); + } + + /** + * Constructor. + * + * @param txBody protobuf TransactionBody + */ + NodeUpdateTransaction(com.hedera.hashgraph.sdk.proto.TransactionBody txBody) { + super(txBody); + initFromTransactionBody(); + } + + /** + * Extract the consensus node identifier in the network state. + * @return the consensus node identifier in the network state. + */ + public long getNodeId() { + return nodeId; + } + + /** + * Assign the consensus node identifier in the network state. + * @param nodeId the consensus node identifier in the network state. + * @return {@code this} + */ + public NodeUpdateTransaction setNodeId(long nodeId) { + requireNotFrozen(); + this.nodeId = nodeId; + return this; + } + + /** + * Extract the Account ID of the Node. + * @return the Account ID of the Node. + */ + public AccountId getAccountId() { + return accountId; + } + + /** + * Assign the Account ID of the Node. + * @param accountId the Account ID of the Node. + * @return {@code this} + */ + public NodeUpdateTransaction setAccountId(AccountId accountId) { + Objects.requireNonNull(accountId); + requireNotFrozen(); + this.accountId = accountId; + return this; + } + + /** + * Extract the description of the node. + * @return the node's description. + */ + @Nullable + public String getDescription() { + return description; + } + + /** + * Sets the description of the node. + * @param description The String to be set as the description of the node. + * @return {@code this} + */ + public NodeUpdateTransaction setDescription(String description) { + requireNotFrozen(); + Objects.requireNonNull(description); + this.description = description; + return this; + } + + /** + * Remove the description contents. + * @return {@code this} + */ + public NodeUpdateTransaction clearDescription() { + requireNotFrozen(); + description = ""; + return this; + } + + /** + * Extract the list of service endpoints for gossip. + * @return the list of service endpoints for gossip. + */ + public List getGossipEndpoints() { + return gossipEndpoints; + } + + /** + * Assign the list of service endpoints for gossip. + * @param gossipEndpoints the list of service endpoints for gossip. + * @return {@code this} + */ + public NodeUpdateTransaction setGossipEndpoints(List gossipEndpoints) { + requireNotFrozen(); + Objects.requireNonNull(gossipEndpoints); + this.gossipEndpoints = new ArrayList<>(gossipEndpoints); + return this; + } + + /** + * Add an endpoint for gossip to the list of service endpoints for gossip. + * @param gossipEndpoint endpoints for gossip to add. + * @return {@code this} + */ + public NodeUpdateTransaction addGossipEndpoint(Endpoint gossipEndpoint) { + requireNotFrozen(); + gossipEndpoints.add(gossipEndpoint); + return this; + } + + /** + * Extract the list of service endpoints for gRPC calls. + * @return the list of service endpoints for gRPC calls. + */ + public List getServiceEndpoints() { + return serviceEndpoints; + } + + /** + * Assign the list of service endpoints for gRPC calls. + * @param serviceEndpoints list of service endpoints for gRPC calls. + * @return {@code this} + */ + public NodeUpdateTransaction setServiceEndpoints(List serviceEndpoints) { + requireNotFrozen(); + Objects.requireNonNull(serviceEndpoints); + this.serviceEndpoints = new ArrayList<>(serviceEndpoints); + return this; + } + + /** + * Add an endpoint for gRPC calls to the list of service endpoints for gRPC calls. + * @param serviceEndpoint endpoints for gRPC calls to add. + * @return {@code this} + */ + public NodeUpdateTransaction addServiceEndpoint(Endpoint serviceEndpoint) { + requireNotFrozen(); + serviceEndpoints.add(serviceEndpoint); + return this; + } + + /** + * Extract the certificate used to sign gossip events. + * @return the DER encoding of the certificate presented. + */ + @Nullable + public byte[] getGossipCaCertificate() { + return gossipCaCertificate != null ? Arrays.copyOf(gossipCaCertificate, gossipCaCertificate.length) : null; + } + + /** + * Sets the certificate used to sign gossip events. + *
+ * This value MUST be the DER encoding of the certificate presented. + * @param gossipCaCertificate the DER encoding of the certificate presented. + * @return {@code this} + */ + public NodeUpdateTransaction setGossipCaCertificate(byte[] gossipCaCertificate) { + Objects.requireNonNull(gossipCaCertificate); + requireNotFrozen(); + this.gossipCaCertificate = Arrays.copyOf(gossipCaCertificate, gossipCaCertificate.length); + return this; + } + + /** + * Extract the hash of the node gRPC TLS certificate. + * @return SHA-384 hash of the node gRPC TLS certificate. + */ + @Nullable + public byte[] getGrpcCertificateHash() { + return grpcCertificateHash != null ? Arrays.copyOf(grpcCertificateHash, grpcCertificateHash.length) : null; + } + + /** + * Sets the hash of the node gRPC TLS certificate. + *
+ * This value MUST be a SHA-384 hash. + * @param grpcCertificateHash SHA-384 hash of the node gRPC TLS certificate. + * @return {@code this} + */ + public NodeUpdateTransaction setGrpcCertificateHash(byte[] grpcCertificateHash) { + Objects.requireNonNull(grpcCertificateHash); + requireNotFrozen(); + this.grpcCertificateHash = Arrays.copyOf(grpcCertificateHash, grpcCertificateHash.length); + return this; + } + + /** + * Get an administrative key controlled by the node operator. + * @return an administrative key controlled by the node operator. + */ + @Nullable + public Key getAdminKey() { + return adminKey; + } + + /** + * Sets an administrative key controlled by the node operator. + * @param adminKey an administrative key to be set. + * @return {@code this} + */ + public NodeUpdateTransaction setAdminKey(Key adminKey) { + Objects.requireNonNull(adminKey); + requireNotFrozen(); + this.adminKey = adminKey; + return this; + } + + /** + * Build the transaction body. + * + * @return {@link com.hedera.hashgraph.sdk.proto.NodeUpdateTransactionBody} + */ + NodeUpdateTransactionBody.Builder build() { + var builder = NodeUpdateTransactionBody.newBuilder(); + + builder.setNodeId(nodeId); + + if (accountId != null) { + builder.setAccountId(accountId.toProtobuf()); + } + + if (description != null) { + builder.setDescription(StringValue.of(description)); + } + + for (Endpoint gossipEndpoint : gossipEndpoints) { + builder.addGossipEndpoint(gossipEndpoint.toProtobuf()); + } + + for (Endpoint serviceEndpoint : serviceEndpoints) { + builder.addServiceEndpoint(serviceEndpoint.toProtobuf()); + } + + if (gossipCaCertificate != null) { + builder.setGossipCaCertificate(BytesValue.of(ByteString.copyFrom(gossipCaCertificate))); + } + + if (grpcCertificateHash != null) { + builder.setGrpcCertificateHash(BytesValue.of(ByteString.copyFrom(grpcCertificateHash))); + } + + if (adminKey != null) { + builder.setAdminKey(adminKey.toProtobufKey()); + } + + return builder; + } + + /** + * Initialize from the transaction body. + */ + void initFromTransactionBody() { + var body = sourceTransactionBody.getNodeUpdate(); + + nodeId = body.getNodeId(); + + if (body.hasAccountId()) { + accountId = AccountId.fromProtobuf(body.getAccountId()); + } + + description = body.getDescription().getValue(); + + for (var gossipEndpoint : body.getGossipEndpointList()) { + gossipEndpoints.add(Endpoint.fromProtobuf(gossipEndpoint)); + } + + for (var serviceEndpoint : body.getServiceEndpointList()) { + serviceEndpoints.add(Endpoint.fromProtobuf(serviceEndpoint)); + } + + gossipCaCertificate = body.getGossipCaCertificate().getValue().toByteArray(); + + grpcCertificateHash = body.getGrpcCertificateHash().getValue().toByteArray(); + + if (body.hasAdminKey()) { + adminKey = Key.fromProtobufKey(body.getAdminKey()); + } + } + + @Override + void validateChecksums(Client client) throws BadEntityIdException { + if (accountId != null) { + accountId.validateChecksum(client); + } + } + + @Override + MethodDescriptor getMethodDescriptor() { + return AddressBookServiceGrpc.getUpdateNodeMethod(); + } + + @Override + void onFreeze(TransactionBody.Builder bodyBuilder) { + bodyBuilder.setNodeUpdate(build()); + } + + @Override + void onScheduled(SchedulableTransactionBody.Builder scheduled) { + scheduled.setNodeUpdate(build()); + } +} 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 ae815d3e06..7fecf87f49 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/Transaction.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/Transaction.java @@ -328,6 +328,9 @@ public static Transaction fromBytes(byte[] bytes) throws InvalidProtocolBuffe case FILECREATE -> new FileCreateTransaction(txs); case FILEDELETE -> new FileDeleteTransaction(txs); case FILEUPDATE -> new FileUpdateTransaction(txs); + case NODECREATE -> new NodeCreateTransaction(txs); + case NODEUPDATE -> new NodeUpdateTransaction(txs); + case NODEDELETE -> new NodeDeleteTransaction(txs); case SYSTEMDELETE -> new SystemDeleteTransaction(txs); case SYSTEMUNDELETE -> new SystemUndeleteTransaction(txs); case FREEZE -> new FreezeTransaction(txs); @@ -398,6 +401,9 @@ public static Transaction fromScheduledTransaction( case FILECREATE -> new FileCreateTransaction(body.setFileCreate(scheduled.getFileCreate()).build()); case FILEDELETE -> new FileDeleteTransaction(body.setFileDelete(scheduled.getFileDelete()).build()); case FILEUPDATE -> new FileUpdateTransaction(body.setFileUpdate(scheduled.getFileUpdate()).build()); + case NODECREATE -> new NodeCreateTransaction(body.setNodeCreate(scheduled.getNodeCreate()).build()); + case NODEUPDATE -> new NodeUpdateTransaction(body.setNodeUpdate(scheduled.getNodeUpdate()).build()); + case NODEDELETE -> new NodeDeleteTransaction(body.setNodeDelete(scheduled.getNodeDelete()).build()); case SYSTEMDELETE -> new SystemDeleteTransaction(body.setSystemDelete(scheduled.getSystemDelete()).build()); case SYSTEMUNDELETE -> new SystemUndeleteTransaction(body.setSystemUndelete(scheduled.getSystemUndelete()).build()); 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 495eb866ac..b2016f5d84 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/TransactionReceipt.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/TransactionReceipt.java @@ -122,6 +122,16 @@ public final class TransactionReceipt { */ public final List serials; + /** + * In the receipt of a NodeCreate, NodeUpdate, NodeDelete, the id of the newly created node. + * An affected node identifier.
+ * This value SHALL be set following a `createNode` transaction.
+ * This value SHALL be set following a `updateNode` transaction.
+ * This value SHALL be set following a `deleteNode` transaction.
+ * This value SHALL NOT be set following any other transaction. + */ + public final long nodeId; + /** * The receipts of processing all transactions with the given id, in consensus time order. */ @@ -148,6 +158,7 @@ public final class TransactionReceipt { @Nullable ScheduleId scheduleId, @Nullable TransactionId scheduledTransactionId, List serials, + long nodeId, List duplicates, List children ) { @@ -165,6 +176,7 @@ public final class TransactionReceipt { this.scheduleId = scheduleId; this.scheduledTransactionId = scheduledTransactionId; this.serials = serials; + this.nodeId = nodeId; this.duplicates = duplicates; this.children = children; } @@ -237,6 +249,8 @@ static TransactionReceipt fromProtobuf( var serials = transactionReceipt.getSerialNumbersList(); + var nodeId = transactionReceipt.getNodeId(); + return new TransactionReceipt( transactionId, status, @@ -252,6 +266,7 @@ static TransactionReceipt fromProtobuf( scheduleId, scheduledTransactionId, serials, + nodeId, duplicates, children ); @@ -358,6 +373,8 @@ com.hedera.hashgraph.sdk.proto.TransactionReceipt toProtobuf() { transactionReceiptBuilder.addSerialNumbers(serial); } + transactionReceiptBuilder.setNodeId(nodeId); + return transactionReceiptBuilder.build(); } @@ -378,6 +395,7 @@ public String toString() { .add("scheduleId", scheduleId) .add("scheduledTransactionId", scheduledTransactionId) .add("serials", serials) + .add("nodeId", nodeId) .add("duplicates", duplicates) .add("children", children) .toString(); diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/AddressBookQueryMockTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/AddressBookQueryMockTest.java index ab4bf6c652..36c7f0e332 100644 --- a/sdk/src/test/java/com/hedera/hashgraph/sdk/AddressBookQueryMockTest.java +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/AddressBookQueryMockTest.java @@ -90,7 +90,9 @@ Endpoint spawnEndpoint() { .setLeft((byte) 0x02) .setRight((byte) 0x03) ) - ).setPort(PORT_NODE_PLAIN); + ) + .setDomainName("unit.test.com") + .setPort(PORT_NODE_PLAIN); } @Test diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.java new file mode 100644 index 0000000000..94547cec69 --- /dev/null +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.java @@ -0,0 +1,250 @@ +/*- + * + * Hedera Java SDK + * + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.hedera.hashgraph.sdk; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.protobuf.ByteString; +import com.hedera.hashgraph.sdk.proto.NodeCreateTransactionBody; +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 java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class NodeCreateTransactionTest { + + 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_GOSSIP_ENDPOINTS = List.of( + spawnTestEndpoint((byte) 0), + spawnTestEndpoint((byte) 1), + spawnTestEndpoint((byte) 2) + ); + + private static final List TEST_SERVICE_ENDPOINTS = List.of( + spawnTestEndpoint((byte) 3), + spawnTestEndpoint((byte) 4), + spawnTestEndpoint((byte) 5), + spawnTestEndpoint((byte) 6) + ); + + private static final byte[] TEST_GOSSIP_CA_CERTIFICATE = new byte[]{0, 1, 2, 3, 4}; + + private static final byte[] TEST_GRPC_CERTIFICATE_HASH = new byte[]{5, 6, 7, 8, 9}; + + private static final PublicKey TEST_ADMIN_KEY = PrivateKey.fromString( + "302e020100300506032b65700422042062c4b69e9f45a554e5424fb5a6fe5e6ac1f19ead31dc7718c2d980fd1f998d4b") + .getPublicKey(); + + final Instant TEST_VALID_START = Instant.ofEpochSecond(1554158542); + + @BeforeAll + public static void beforeAll() { + SnapshotMatcher.start(); + } + + @AfterAll + public static void afterAll() { + SnapshotMatcher.validateSnapshots(); + } + + @Test + void shouldSerialize() { + SnapshotMatcher.expect(spawnTestTransaction().toString()).toMatchSnapshot(); + } + + private static Endpoint spawnTestEndpoint(byte offset) { + return new Endpoint() + .setAddress( + new IPv4Address() + .setNetwork( + new IPv4AddressPart() + .setLeft((byte) (0x00 + offset)) + .setRight((byte) (0x01 + offset)) + ).setHost( + new IPv4AddressPart() + .setLeft((byte) (0x02 + offset)) + .setRight((byte) (0x03 + offset)) + ) + ) + .setDomainName(offset + "unit.test.com") + .setPort(42 + offset); + } + + private NodeCreateTransaction spawnTestTransaction() { + return new NodeCreateTransaction() + .setNodeAccountIds( + Arrays.asList(AccountId.fromString("0.0.5005"), AccountId.fromString("0.0.5006"))) + .setTransactionId(TransactionId.withValidStart(AccountId.fromString("0.0.5006"), TEST_VALID_START)) + .setAccountId(TEST_ACCOUNT_ID) + .setDescription(TEST_DESCRIPTION) + .setGossipEndpoints(TEST_GOSSIP_ENDPOINTS) + .setServiceEndpoints(TEST_SERVICE_ENDPOINTS) + .setGossipCaCertificate(TEST_GOSSIP_CA_CERTIFICATE) + .setGrpcCertificateHash(TEST_GRPC_CERTIFICATE_HASH) + .setAdminKey(TEST_ADMIN_KEY) + .setMaxTransactionFee(new Hbar(1)) + .freeze() + .sign(TEST_PRIVATE_KEY); + } + + @Test + void shouldBytes() throws Exception { + var tx = spawnTestTransaction(); + var tx2 = NodeCreateTransaction.fromBytes(tx.toBytes()); + assertThat(tx2.toString()).isEqualTo(tx.toString()); + } + + @Test + void fromScheduledTransaction() { + var transactionBody = SchedulableTransactionBody.newBuilder() + .setNodeCreate(NodeCreateTransactionBody.newBuilder().build()).build(); + + var tx = Transaction.fromScheduledTransaction(transactionBody); + + assertThat(tx).isInstanceOf(NodeCreateTransaction.class); + } + + @Test + void constructNodeCreateTransactionFromTransactionBodyProtobuf() { + var transactionBodyBuilder = NodeCreateTransactionBody.newBuilder(); + + transactionBodyBuilder.setAccountId(TEST_ACCOUNT_ID.toProtobuf()); + transactionBodyBuilder.setDescription(TEST_DESCRIPTION); + + for (Endpoint gossipEndpoint : TEST_GOSSIP_ENDPOINTS) { + transactionBodyBuilder.addGossipEndpoint(gossipEndpoint.toProtobuf()); + } + + for (Endpoint serviceEndpoint : TEST_SERVICE_ENDPOINTS) { + transactionBodyBuilder.addServiceEndpoint(serviceEndpoint.toProtobuf()); + } + + transactionBodyBuilder.setGossipCaCertificate(ByteString.copyFrom(TEST_GOSSIP_CA_CERTIFICATE)); + transactionBodyBuilder.setGrpcCertificateHash(ByteString.copyFrom(TEST_GRPC_CERTIFICATE_HASH)); + transactionBodyBuilder.setAdminKey(TEST_ADMIN_KEY.toProtobufKey()); + + var tx = TransactionBody.newBuilder().setNodeCreate(transactionBodyBuilder.build()).build(); + var nodeCreateTransaction = new NodeCreateTransaction(tx); + + assertThat(nodeCreateTransaction.getAccountId()).isEqualTo(TEST_ACCOUNT_ID); + assertThat(nodeCreateTransaction.getDescription()).isEqualTo(TEST_DESCRIPTION); + assertThat(nodeCreateTransaction.getGossipEndpoints()).hasSize(TEST_GOSSIP_ENDPOINTS.size()); + assertThat(nodeCreateTransaction.getServiceEndpoints()).hasSize(TEST_SERVICE_ENDPOINTS.size()); + assertThat(nodeCreateTransaction.getGossipCaCertificate()).isEqualTo(TEST_GOSSIP_CA_CERTIFICATE); + assertThat(nodeCreateTransaction.getGrpcCertificateHash()).isEqualTo(TEST_GRPC_CERTIFICATE_HASH); + assertThat(nodeCreateTransaction.getAdminKey()).isEqualTo(TEST_ADMIN_KEY); + } + + @Test + void getSetAccountId() { + var nodeCreateTransaction = new NodeCreateTransaction().setAccountId(TEST_ACCOUNT_ID); + assertThat(nodeCreateTransaction.getAccountId()).isEqualTo(TEST_ACCOUNT_ID); + } + + @Test + void getSetAccountIdFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setAccountId(TEST_ACCOUNT_ID)); + } + + @Test + void getSetDescription() { + var nodeCreateTransaction = new NodeCreateTransaction().setDescription(TEST_DESCRIPTION); + assertThat(nodeCreateTransaction.getDescription()).isEqualTo(TEST_DESCRIPTION); + } + + @Test + void getSetDescriptionFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setDescription(TEST_DESCRIPTION)); + } + + @Test + void getSetGossipEndpoints() { + var nodeCreateTransaction = new NodeCreateTransaction().setGossipEndpoints(TEST_GOSSIP_ENDPOINTS); + assertThat(nodeCreateTransaction.getGossipEndpoints()).isEqualTo(TEST_GOSSIP_ENDPOINTS); + } + + @Test + void setTestGossipEndpointsFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setGossipEndpoints(TEST_GOSSIP_ENDPOINTS)); + } + + @Test + void getSetServiceEndpoints() { + var nodeCreateTransaction = new NodeCreateTransaction().setServiceEndpoints(TEST_SERVICE_ENDPOINTS); + assertThat(nodeCreateTransaction.getServiceEndpoints()).isEqualTo(TEST_SERVICE_ENDPOINTS); + } + + @Test + void getSetServiceEndpointsFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setServiceEndpoints(TEST_SERVICE_ENDPOINTS)); + } + + @Test + void getSetGossipCaCertificate() { + var nodeCreateTransaction = new NodeCreateTransaction().setGossipCaCertificate(TEST_GOSSIP_CA_CERTIFICATE); + assertThat(nodeCreateTransaction.getGossipCaCertificate()).isEqualTo(TEST_GOSSIP_CA_CERTIFICATE); + } + + @Test + void getSetGossipCaCertificateFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setGossipCaCertificate(TEST_GOSSIP_CA_CERTIFICATE)); + } + + @Test + void getSetGrpcCertificateHash() { + var nodeCreateTransaction = new NodeCreateTransaction().setGrpcCertificateHash(TEST_GRPC_CERTIFICATE_HASH); + assertThat(nodeCreateTransaction.getGrpcCertificateHash()).isEqualTo(TEST_GRPC_CERTIFICATE_HASH); + } + + @Test + void getSetGrpcCertificateHashFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setGrpcCertificateHash(TEST_GRPC_CERTIFICATE_HASH)); + } + + @Test + void getSetAdminKey() { + var nodeCreateTransaction = new NodeCreateTransaction().setAdminKey(TEST_ADMIN_KEY); + assertThat(nodeCreateTransaction.getAdminKey()).isEqualTo(TEST_ADMIN_KEY); + } + + @Test + void getSetAdminKeyFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setAdminKey(TEST_ADMIN_KEY)); + } +} diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.snap new file mode 100644 index 0000000000..48b25e5db5 --- /dev/null +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeCreateTransactionTest.snap @@ -0,0 +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 ip_address_v4: \"\\000\\001\\002\\003\"\n port: 42\n }\n gossip_endpoint {\n domain_name: \"1unit.test.com\"\n ip_address_v4: \"\\001\\002\\003\\004\"\n port: 43\n }\n gossip_endpoint {\n domain_name: \"2unit.test.com\"\n ip_address_v4: \"\\002\\003\\004\\005\"\n port: 44\n }\n grpc_certificate_hash: \"\\005\\006\\a\\b\\t\"\n service_endpoint {\n domain_name: \"3unit.test.com\"\n ip_address_v4: \"\\003\\004\\005\\006\"\n port: 45\n }\n service_endpoint {\n domain_name: \"4unit.test.com\"\n ip_address_v4: \"\\004\\005\\006\\a\"\n port: 46\n }\n service_endpoint {\n domain_name: \"5unit.test.com\"\n ip_address_v4: \"\\005\\006\\a\\b\"\n port: 47\n }\n service_endpoint {\n domain_name: \"6unit.test.com\"\n ip_address_v4: \"\\006\\a\\b\\t\"\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/NodeDeleteTransactionTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeDeleteTransactionTest.java new file mode 100644 index 0000000000..f55716e4b9 --- /dev/null +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeDeleteTransactionTest.java @@ -0,0 +1,110 @@ +/*- + * + * Hedera Java SDK + * + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.hedera.hashgraph.sdk; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.hedera.hashgraph.sdk.proto.NodeDeleteTransactionBody; +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 NodeDeleteTransactionTest { + + private static final PrivateKey TEST_PRIVATE_KEY = PrivateKey.fromString( + "302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c97732394482538e10"); + + private static final long TEST_NODE_ID = 420; + + final Instant TEST_VALID_START = Instant.ofEpochSecond(1554158542); + + @BeforeAll + public static void beforeAll() { + SnapshotMatcher.start(); + } + + @AfterAll + public static void afterAll() { + SnapshotMatcher.validateSnapshots(); + } + + @Test + void shouldSerialize() { + SnapshotMatcher.expect(spawnTestTransaction().toString()).toMatchSnapshot(); + } + + private NodeDeleteTransaction spawnTestTransaction() { + return new NodeDeleteTransaction() + .setNodeAccountIds( + Arrays.asList(AccountId.fromString("0.0.5005"), AccountId.fromString("0.0.5006"))) + .setTransactionId(TransactionId.withValidStart(AccountId.fromString("0.0.5006"), TEST_VALID_START)) + .setNodeId(TEST_NODE_ID) + .setMaxTransactionFee(new Hbar(1)) + .freeze() + .sign(TEST_PRIVATE_KEY); + } + + @Test + void shouldBytes() throws Exception { + var tx = spawnTestTransaction(); + var tx2 = NodeDeleteTransaction.fromBytes(tx.toBytes()); + assertThat(tx2.toString()).isEqualTo(tx.toString()); + } + + @Test + void fromScheduledTransaction() { + var transactionBody = SchedulableTransactionBody.newBuilder() + .setNodeDelete(NodeDeleteTransactionBody.newBuilder().build()).build(); + + var tx = Transaction.fromScheduledTransaction(transactionBody); + + assertThat(tx).isInstanceOf(NodeDeleteTransaction.class); + } + + @Test + void constructNodeDeleteTransactionFromTransactionBodyProtobuf() { + var transactionBodyBuilder = NodeDeleteTransactionBody.newBuilder(); + + transactionBodyBuilder.setNodeId(TEST_NODE_ID); + + var tx = TransactionBody.newBuilder().setNodeDelete(transactionBodyBuilder.build()).build(); + var nodeDeleteTransaction = new NodeDeleteTransaction(tx); + + assertThat(nodeDeleteTransaction.getNodeId()).isEqualTo(TEST_NODE_ID); + } + + @Test + void getSetNodeId() { + var nodeDeleteTransaction = new NodeDeleteTransaction().setNodeId(TEST_NODE_ID); + assertThat(nodeDeleteTransaction.getNodeId()).isEqualTo(TEST_NODE_ID); + } + + @Test + void getSetNodeIdFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setNodeId(TEST_NODE_ID)); + } +} diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeDeleteTransactionTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeDeleteTransactionTest.snap new file mode 100644 index 0000000000..456b6ad72c --- /dev/null +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeDeleteTransactionTest.snap @@ -0,0 +1,3 @@ +com.hedera.hashgraph.sdk.NodeDeleteTransactionTest.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_delete {\n 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/NodeUpdateTransactionTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.java new file mode 100644 index 0000000000..e47c1a71b1 --- /dev/null +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.java @@ -0,0 +1,270 @@ +/*- + * + * Hedera Java SDK + * + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.hedera.hashgraph.sdk; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.StringValue; +import com.hedera.hashgraph.sdk.proto.NodeUpdateTransactionBody; +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 java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class NodeUpdateTransactionTest { + + private static final PrivateKey TEST_PRIVATE_KEY = PrivateKey.fromString( + "302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c97732394482538e10"); + + private static final long TEST_NODE_ID = 420; + + 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_GOSSIP_ENDPOINTS = List.of( + spawnTestEndpoint((byte) 0), + spawnTestEndpoint((byte) 1), + spawnTestEndpoint((byte) 2) + ); + + private static final List TEST_SERVICE_ENDPOINTS = List.of( + spawnTestEndpoint((byte) 3), + spawnTestEndpoint((byte) 4), + spawnTestEndpoint((byte) 5), + spawnTestEndpoint((byte) 6) + ); + + private static final byte[] TEST_GOSSIP_CA_CERTIFICATE = new byte[]{0, 1, 2, 3, 4}; + + private static final byte[] TEST_GRPC_CERTIFICATE_HASH = new byte[]{5, 6, 7, 8, 9}; + + private static final PublicKey TEST_ADMIN_KEY = PrivateKey.fromString( + "302e020100300506032b65700422042062c4b69e9f45a554e5424fb5a6fe5e6ac1f19ead31dc7718c2d980fd1f998d4b") + .getPublicKey(); + + final Instant TEST_VALID_START = Instant.ofEpochSecond(1554158542); + + @BeforeAll + public static void beforeAll() { + SnapshotMatcher.start(); + } + + @AfterAll + public static void afterAll() { + SnapshotMatcher.validateSnapshots(); + } + + @Test + void shouldSerialize() { + SnapshotMatcher.expect(spawnTestTransaction().toString()).toMatchSnapshot(); + } + + private static Endpoint spawnTestEndpoint(byte offset) { + return new Endpoint() + .setAddress( + new IPv4Address() + .setNetwork( + new IPv4AddressPart() + .setLeft((byte) (0x00 + offset)) + .setRight((byte) (0x01 + offset)) + ).setHost( + new IPv4AddressPart() + .setLeft((byte) (0x02 + offset)) + .setRight((byte) (0x03 + offset)) + ) + ) + .setDomainName(offset + "unit.test.com") + .setPort(42 + offset); + } + + private NodeUpdateTransaction spawnTestTransaction() { + return new NodeUpdateTransaction() + .setNodeAccountIds( + Arrays.asList(AccountId.fromString("0.0.5005"), AccountId.fromString("0.0.5006"))) + .setTransactionId(TransactionId.withValidStart(AccountId.fromString("0.0.5006"), TEST_VALID_START)) + .setNodeId(TEST_NODE_ID) + .setAccountId(TEST_ACCOUNT_ID) + .setAccountId(TEST_ACCOUNT_ID) + .setDescription(TEST_DESCRIPTION) + .setGossipEndpoints(TEST_GOSSIP_ENDPOINTS) + .setServiceEndpoints(TEST_SERVICE_ENDPOINTS) + .setGossipCaCertificate(TEST_GOSSIP_CA_CERTIFICATE) + .setGrpcCertificateHash(TEST_GRPC_CERTIFICATE_HASH) + .setAdminKey(TEST_ADMIN_KEY) + .setMaxTransactionFee(new Hbar(1)) + .freeze() + .sign(TEST_PRIVATE_KEY); + } + + @Test + void shouldBytes() throws Exception { + var tx = spawnTestTransaction(); + var tx2 = NodeUpdateTransaction.fromBytes(tx.toBytes()); + assertThat(tx2.toString()).isEqualTo(tx.toString()); + } + + @Test + void fromScheduledTransaction() { + var transactionBody = SchedulableTransactionBody.newBuilder() + .setNodeUpdate(NodeUpdateTransactionBody.newBuilder().build()).build(); + + var tx = Transaction.fromScheduledTransaction(transactionBody); + + assertThat(tx).isInstanceOf(NodeUpdateTransaction.class); + } + + @Test + void constructNodeUpdateTransactionFromTransactionBodyProtobuf() { + var transactionBodyBuilder = NodeUpdateTransactionBody.newBuilder(); + + transactionBodyBuilder.setNodeId(TEST_NODE_ID); + transactionBodyBuilder.setAccountId(TEST_ACCOUNT_ID.toProtobuf()); + transactionBodyBuilder.setDescription(StringValue.of(TEST_DESCRIPTION)); + + for (Endpoint gossipEndpoint : TEST_GOSSIP_ENDPOINTS) { + transactionBodyBuilder.addGossipEndpoint(gossipEndpoint.toProtobuf()); + } + + for (Endpoint serviceEndpoint : TEST_SERVICE_ENDPOINTS) { + transactionBodyBuilder.addServiceEndpoint(serviceEndpoint.toProtobuf()); + } + + transactionBodyBuilder.setGossipCaCertificate(BytesValue.of(ByteString.copyFrom(TEST_GOSSIP_CA_CERTIFICATE))); + transactionBodyBuilder.setGrpcCertificateHash(BytesValue.of(ByteString.copyFrom(TEST_GRPC_CERTIFICATE_HASH))); + transactionBodyBuilder.setAdminKey(TEST_ADMIN_KEY.toProtobufKey()); + + var tx = TransactionBody.newBuilder().setNodeUpdate(transactionBodyBuilder.build()).build(); + var nodeUpdateTransaction = new NodeUpdateTransaction(tx); + + assertThat(nodeUpdateTransaction.getNodeId()).isEqualTo(TEST_NODE_ID); + assertThat(nodeUpdateTransaction.getAccountId()).isEqualTo(TEST_ACCOUNT_ID); + assertThat(nodeUpdateTransaction.getDescription()).isEqualTo(TEST_DESCRIPTION); + assertThat(nodeUpdateTransaction.getGossipEndpoints()).hasSize(TEST_GOSSIP_ENDPOINTS.size()); + assertThat(nodeUpdateTransaction.getServiceEndpoints()).hasSize(TEST_SERVICE_ENDPOINTS.size()); + assertThat(nodeUpdateTransaction.getGossipCaCertificate()).isEqualTo(TEST_GOSSIP_CA_CERTIFICATE); + assertThat(nodeUpdateTransaction.getGrpcCertificateHash()).isEqualTo(TEST_GRPC_CERTIFICATE_HASH); + assertThat(nodeUpdateTransaction.getAdminKey()).isEqualTo(TEST_ADMIN_KEY); + } + + @Test + void getSetNodeId() { + var nodeUpdateTransaction = new NodeUpdateTransaction().setNodeId(TEST_NODE_ID); + assertThat(nodeUpdateTransaction.getNodeId()).isEqualTo(TEST_NODE_ID); + } + + @Test + void getSetNodeIdFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setNodeId(TEST_NODE_ID)); + } + + @Test + void getSetAccountId() { + var nodeUpdateTransaction = new NodeUpdateTransaction().setAccountId(TEST_ACCOUNT_ID); + assertThat(nodeUpdateTransaction.getAccountId()).isEqualTo(TEST_ACCOUNT_ID); + } + + @Test + void getSetAccountIdFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setAccountId(TEST_ACCOUNT_ID)); + } + + @Test + void getSetDescription() { + var nodeUpdateTransaction = new NodeUpdateTransaction().setDescription(TEST_DESCRIPTION); + assertThat(nodeUpdateTransaction.getDescription()).isEqualTo(TEST_DESCRIPTION); + } + + @Test + void getSetDescriptionFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setDescription(TEST_DESCRIPTION)); + } + + @Test + void getSetGossipEndpoints() { + var nodeUpdateTransaction = new NodeUpdateTransaction().setGossipEndpoints(TEST_GOSSIP_ENDPOINTS); + assertThat(nodeUpdateTransaction.getGossipEndpoints()).isEqualTo(TEST_GOSSIP_ENDPOINTS); + } + + @Test + void setTestGossipEndpointsFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setGossipEndpoints(TEST_GOSSIP_ENDPOINTS)); + } + + @Test + void getSetServiceEndpoints() { + var nodeUpdateTransaction = new NodeUpdateTransaction().setServiceEndpoints(TEST_SERVICE_ENDPOINTS); + assertThat(nodeUpdateTransaction.getServiceEndpoints()).isEqualTo(TEST_SERVICE_ENDPOINTS); + } + + @Test + void getSetServiceEndpointsFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setServiceEndpoints(TEST_SERVICE_ENDPOINTS)); + } + + @Test + void getSetGossipCaCertificate() { + var nodeUpdateTransaction = new NodeUpdateTransaction().setGossipCaCertificate(TEST_GOSSIP_CA_CERTIFICATE); + assertThat(nodeUpdateTransaction.getGossipCaCertificate()).isEqualTo(TEST_GOSSIP_CA_CERTIFICATE); + } + + @Test + void getSetGossipCaCertificateFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setGossipCaCertificate(TEST_GOSSIP_CA_CERTIFICATE)); + } + + @Test + void getSetGrpcCertificateHash() { + var nodeUpdateTransaction = new NodeUpdateTransaction().setGrpcCertificateHash(TEST_GRPC_CERTIFICATE_HASH); + assertThat(nodeUpdateTransaction.getGrpcCertificateHash()).isEqualTo(TEST_GRPC_CERTIFICATE_HASH); + } + + @Test + void getSetGrpcCertificateHashFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setGrpcCertificateHash(TEST_GRPC_CERTIFICATE_HASH)); + } + + @Test + void getSetAdminKey() { + var nodeUpdateTransaction = new NodeUpdateTransaction().setAdminKey(TEST_ADMIN_KEY); + assertThat(nodeUpdateTransaction.getAdminKey()).isEqualTo(TEST_ADMIN_KEY); + } + + @Test + void getSetAdminKeyFrozen() { + var tx = spawnTestTransaction(); + assertThrows(IllegalStateException.class, () -> tx.setAdminKey(TEST_ADMIN_KEY)); + } +} diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.snap new file mode 100644 index 0000000000..6f01573f9a --- /dev/null +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/NodeUpdateTransactionTest.snap @@ -0,0 +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 description {\n value: \"Test description\"\n }\n gossip_ca_certificate {\n value: \"\\000\\001\\002\\003\\004\"\n }\n gossip_endpoint {\n domain_name: \"0unit.test.com\"\n ip_address_v4: \"\\000\\001\\002\\003\"\n port: 42\n }\n gossip_endpoint {\n domain_name: \"1unit.test.com\"\n ip_address_v4: \"\\001\\002\\003\\004\"\n port: 43\n }\n gossip_endpoint {\n domain_name: \"2unit.test.com\"\n ip_address_v4: \"\\002\\003\\004\\005\"\n port: 44\n }\n grpc_certificate_hash {\n value: \"\\005\\006\\a\\b\\t\"\n }\n node_id: 420\n service_endpoint {\n domain_name: \"3unit.test.com\"\n ip_address_v4: \"\\003\\004\\005\\006\"\n port: 45\n }\n service_endpoint {\n domain_name: \"4unit.test.com\"\n ip_address_v4: \"\\004\\005\\006\\a\"\n port: 46\n }\n service_endpoint {\n domain_name: \"5unit.test.com\"\n ip_address_v4: \"\\005\\006\\a\\b\"\n port: 47\n }\n service_endpoint {\n domain_name: \"6unit.test.com\"\n ip_address_v4: \"\\006\\a\\b\\t\"\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/TransactionReceiptTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionReceiptTest.java index b09a67fbe4..97cc780b4f 100644 --- a/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionReceiptTest.java +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionReceiptTest.java @@ -43,6 +43,7 @@ static TransactionReceipt spawnReceiptExample() { ScheduleId.fromString("1.1.1"), TransactionId.withValidStart(AccountId.fromString("3.3.3"), time), List.of(1L, 2L, 3L), + 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 f3615e94fd..9a19e2b5d3 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}, 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], duplicates=[], children=[]}" -] + "TransactionReceipt{transactionId=null, status=SCHEDULE_ALREADY_DELETED, exchangeRate=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=[]}" +] \ 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 0af738e60e..152bccc1c7 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}, 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], 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}" + "TransactionRecord{receipt=TransactionReceipt{transactionId=null, status=SCHEDULE_ALREADY_DELETED, exchangeRate=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}" ] 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}, 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], 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}" + "TransactionRecord{receipt=TransactionReceipt{transactionId=null, status=SCHEDULE_ALREADY_DELETED, exchangeRate=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}" ] \ No newline at end of file