Skip to content

Commit 934baf1

Browse files
authored
feat: add NodeCreateTransaction (#346)
* feat: implement NodeCreateTransaction Signed-off-by: dosi <dosi.kolev@limechain.tech> * feat: add integraton tests Signed-off-by: dosi <dosi.kolev@limechain.tech> * test: add unit test for node create Signed-off-by: dosi <dosi.kolev@limechain.tech> * docs: add node create example Signed-off-by: dosi <dosi.kolev@limechain.tech> * feat: add node_id property in TransactionReceipt Signed-off-by: dosi <dosi.kolev@limechain.tech> * docs: fix running_examples transaction order Signed-off-by: dosi <dosi.kolev@limechain.tech> * docs: add NodeCreateTransaction in running_examples Signed-off-by: dosi <dosi.kolev@limechain.tech> * chore: add NodeCreateTransaction to __init__.py Signed-off-by: dosi <dosi.kolev@limechain.tech> * chore: update changelog Signed-off-by: dosi <dosi.kolev@limechain.tech> * chore: address PR feedback Signed-off-by: dosi <dosi.kolev@limechain.tech> * feat: add node create transaction build_scheduled_body() and unit test Signed-off-by: dosi <dosi.kolev@limechain.tech> --------- Signed-off-by: dosi <dosi.kolev@limechain.tech>
1 parent 1838a65 commit 934baf1

File tree

9 files changed

+901
-69
lines changed

9 files changed

+901
-69
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
1010
- Convert camelCase to snake_case in integration tests (#318)
1111

1212
### Added
13+
- Exposed node_id property in `TransactionReceipt`
14+
- NodeCreateTransaction class
1315
- ScheduleId() class
1416
- ScheduleCreateTransaction() class
1517
- build_scheduled_body() in every transaction

docs/sdk_users/running_examples.md

Lines changed: 55 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ You can choose either syntax or even mix both styles in your projects.
6767
- [Executing Ethereum Transactions](#executing-ethereum-transactions)
6868
- [Schedule Transactions](#schedule-transactions)
6969
- [Creating a Schedule](#creating-a-schedule)
70+
- [Node Transactions](#node-transactions)
71+
- [Creating a Node](#creating-a-node)
7072
- [Miscellaneous Queries](#miscellaneous-queries)
7173
- [Querying Transaction Record](#querying-transaction-record)
7274

@@ -1522,75 +1524,28 @@ transaction.sign(admin_key) # Admin key must have been set during contract crea
15221524
transaction.execute(client)
15231525
```
15241526

1525-
## Schedule Transactions
1526-
1527-
### Creating a Schedule
1527+
### Executing Ethereum Transactions
15281528

15291529
#### Pythonic Syntax:
15301530
```python
1531-
# First, create a transaction to be scheduled (e.g., a transfer transaction)
1532-
transfer_tx = TransferTransaction(
1533-
hbar_transfers={
1534-
sender_id: -amount, # Negative amount = debit
1535-
recipient_id: amount # Positive amount = credit
1536-
}
1537-
)
1538-
1539-
# There are two equivalent approaches to creating scheduled transactions:
1540-
1541-
# Approach 1: Use transaction.schedule() to generate a pre-configured ScheduleCreateTransaction
1542-
# This internally creates a ScheduleCreateTransaction and sets the scheduled transaction
1543-
schedule_tx1 = transfer_tx.schedule()
1544-
# Then you can use the setter methods to configure additional parameters
1545-
# schedule_tx1.set_payer_account_id(...).set_admin_key(...).etc
1546-
1547-
# Approach 2: Create a ScheduleCreateTransaction with params directly
1548-
schedule_tx = ScheduleCreateTransaction(
1549-
schedule_params=ScheduleCreateParams(
1550-
payer_account_id=client.operator_account_id,
1551-
admin_key=admin_key.public_key(),
1552-
expiration_time=expiration_time, # Timestamp when the schedule expires
1553-
wait_for_expiry=True # If true, executes only when expired even if all signatures present
1554-
)
1555-
)
1556-
# Set the transaction to be scheduled
1557-
schedule_tx.set_scheduled_transaction(transfer_tx)
1558-
schedule_tx.freeze_with(client)
1531+
# You must provide signed Ethereum transaction data (see how to generate this in examples/ethereum_transaction_execute.py)
1532+
transaction = EthereumTransaction(
1533+
ethereum_data=ethereum_transaction_data
1534+
).freeze_with(client)
15591535

1560-
# Sign with required keys (any account being debited must sign)
1561-
schedule_tx.sign(sender_private_key)
1562-
schedule_tx.sign(admin_key)
1563-
schedule_tx.sign(payer_account_private_key) # Sign with the payer key
1536+
transaction.execute(client)
15641537
```
15651538

15661539
#### Method Chaining:
15671540
```python
1568-
# First, create a transaction to be scheduled (e.g., a transfer transaction)
1569-
transfer_tx = (
1570-
TransferTransaction()
1571-
.add_hbar_transfer(sender_id, -amount) # Negative amount = debit
1572-
.add_hbar_transfer(recipient_id, amount) # Positive amount = credit
1573-
)
1574-
1575-
# Convert it to a scheduled transaction
1576-
# Using schedule() is equivalent to:
1577-
# schedule_tx = ScheduleCreateTransaction()
1578-
# schedule_tx.set_scheduled_transaction(transfer_tx)
1579-
schedule_tx = transfer_tx.schedule()
1580-
1581-
# Configure the scheduled transaction
1582-
receipt = (
1583-
schedule_tx
1584-
.set_payer_account_id(payer_account_id)
1585-
.set_admin_key(admin_key.public_key())
1586-
.set_expiration_time(expiration_time) # Timestamp when the schedule expires
1587-
.set_wait_for_expiry(True) # If true, executes only when expired even if all signatures present
1541+
# You must provide signed Ethereum transaction data (see how to generate this in examples/ethereum_transaction_execute.py)
1542+
transaction = (
1543+
EthereumTransaction()
1544+
.set_ethereum_data(ethereum_transaction_data)
15881545
.freeze_with(client)
1589-
.sign(sender_private_key) # Sign with the account being debited
1590-
.sign(admin_key) # Sign with the admin key
1591-
.sign(payer_account_private_key) # Sign with the payer key
1592-
.execute(client)
15931546
)
1547+
1548+
transaction.execute(client)
15941549
```
15951550

15961551
## Schedule Transactions
@@ -1664,31 +1619,62 @@ receipt = (
16641619
)
16651620
```
16661621

1667-
### Executing Ethereum Transactions
1622+
## Node Transactions
1623+
1624+
### Creating a Node
1625+
1626+
> **IMPORTANT**: Node creation is a privileged transaction only available on local development networks like "solo". Regular developers do not have permission to create nodes on testnet or mainnet as this operation requires special authorization.
16681627
16691628
#### Pythonic Syntax:
16701629
```python
1671-
# You must provide signed Ethereum transaction data (see how to generate this in examples/ethereum_transaction_execute.py)
1672-
transaction = EthereumTransaction(
1673-
ethereum_data=ethereum_transaction_data
1630+
transaction = NodeCreateTransaction(
1631+
node_create_params=NodeCreateParams(
1632+
account_id=account_id,
1633+
description="Example node",
1634+
gossip_endpoints=[
1635+
Endpoint(domain_name="gossip1.example.com", port=50211),
1636+
Endpoint(domain_name="gossip2.example.com", port=50212)
1637+
],
1638+
service_endpoints=[
1639+
Endpoint(domain_name="service1.example.com", port=50211),
1640+
Endpoint(domain_name="service2.example.com", port=50212)
1641+
],
1642+
gossip_ca_certificate=gossip_ca_cert,
1643+
admin_key=admin_key.public_key(),
1644+
decline_reward=True,
1645+
grpc_web_proxy_endpoint=Endpoint(domain_name="grpc.example.com", port=50213)
1646+
)
16741647
).freeze_with(client)
16751648

1676-
transaction.execute(client)
1649+
transaction.sign(admin_key) # Sign with admin key
1650+
receipt = transaction.execute(client)
16771651
```
16781652

16791653
#### Method Chaining:
16801654
```python
1681-
# You must provide signed Ethereum transaction data (see how to generate this in examples/ethereum_transaction_execute.py)
16821655
transaction = (
1683-
EthereumTransaction()
1684-
.set_ethereum_data(ethereum_transaction_data)
1656+
NodeCreateTransaction()
1657+
.set_account_id(account_id)
1658+
.set_description("Example node")
1659+
.set_gossip_endpoints([
1660+
Endpoint(domain_name="gossip1.example.com", port=50211),
1661+
Endpoint(domain_name="gossip2.example.com", port=50212)
1662+
])
1663+
.set_service_endpoints([
1664+
Endpoint(domain_name="service1.example.com", port=50211),
1665+
Endpoint(domain_name="service2.example.com", port=50212)
1666+
])
1667+
.set_gossip_ca_certificate(gossip_ca_cert)
1668+
.set_admin_key(admin_key.public_key())
1669+
.set_grpc_web_proxy_endpoint(Endpoint(domain_name="grpc.example.com", port=50213))
1670+
.set_decline_reward(True)
16851671
.freeze_with(client)
16861672
)
16871673

1688-
transaction.execute(client)
1674+
transaction.sign(admin_key) # Sign with the admin key
1675+
receipt = transaction.execute(client)
16891676
```
16901677

1691-
16921678
## Miscellaneous Queries
16931679

16941680
### Querying Transaction Record

examples/node_create.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""
2+
Demonstrates adding a node on the network.
3+
4+
NOTE: This is a privileged transaction that can only be executed on solo but not on local-node.
5+
Regular developers do not have the required permissions to create
6+
nodes on testnet or mainnet as this operation requires special authorization.
7+
8+
This example is provided to demonstrate the API for educational purposes or for use
9+
in private network deployments where you have the necessary administrative privileges.
10+
11+
Solo is a local development network for Hedera that allows you to run a complete
12+
Hedera network on your machine for testing and development purposes.
13+
14+
Setup options:
15+
1. GitHub repository with full setup instructions: https://github.com/hiero-ledger/solo
16+
2. Official documentation with step-by-step guide: https://solo.hiero.org/v0.43.0/docs/step-by-step-guide/
17+
"""
18+
19+
import sys
20+
21+
from dotenv import load_dotenv
22+
23+
from hiero_sdk_python import AccountId, Client, Network, PrivateKey
24+
from hiero_sdk_python.address_book.endpoint import Endpoint
25+
from hiero_sdk_python.nodes.node_create_transaction import NodeCreateTransaction
26+
from hiero_sdk_python.response_code import ResponseCode
27+
28+
# Gossip certificate is a DER-encoded x509 certificate used for secure communication between nodes.
29+
# This certificate authenticates the node's identity during gossip protocol communication.
30+
# Information about x509 certificates: https://www.ssl.com/faqs/what-is-an-x-509-certificate/
31+
# This command creates a self-signed certificate suitable for development/testing:
32+
# 1. Generate a self-signed certificate with RSA key
33+
# 2. Convert it to DER format (required by Hedera)
34+
# 3. Convert to hex format for use in code
35+
# openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes -subj "/CN=solo" -keyout gossip.key -out gossip.pem && openssl x509 -in gossip.pem -outform DER | xxd -p -c 32 > gossip.hex
36+
GOSSIP_CERTIFICATE = "3082052830820310a003020102020101300d06092a864886f70d01010c05003010310e300c060355040313056e6f6465333024170d3234313030383134333233395a181332313234313030383134333233392e3337395a3010310e300c060355040313056e6f64653330820222300d06092a864886f70d01010105000382020f003082020a0282020100af111cff0c4ad8125d2f4b8691ce87332fecc867f7a94ddc0f3f96514cc4224d44af516394f7384c1ef0a515d29aa6116b65bc7e4d7e2d848cf79fbfffedae3a6583b3957a438bdd780c4981b800676ea509bc8c619ae04093b5fc642c4484152f0e8bcaabf19eae025b630028d183a2f47caf6d9f1075efb30a4248679d871beef1b7e9115382270cbdb68682fae4b1fd592cadb414d918c0a8c23795c7c5a91e22b3e90c410825a2bc1a840efc5bf9976a7f474c7ed7dc047e4ddd2db631b68bb4475f173baa3edc234c4bed79c83e2f826f79e07d0aade2d984da447a8514135bfa4145274a7f62959a23c4f0fae5adc6855974e7c04164951d052beb5d45cb1f3cdfd005da894dea9151cb62ba43f4731c6bb0c83e10fd842763ba6844ef499f71bc67fa13e4917fb39f2ad18112170d31cdcb3c61c9e3253accf703dbd8427fdcb87ece78b787b6cfdc091e8fedea8ad95dc64074e1fc6d0e42ea2337e18a5e54e4aaab3791a98dfcef282e2ae1caec9cf986fabe8f36e6a21c8711647177e492d264415e765a86c58599cd97b103cb4f6a01d2edd06e3b60470cf64daca7aecf831197b466cae04baeeac19840a05394bef628aed04b611cfa13677724b08ddfd662b02fd0ef0af17eb7f4fb8c1c17fbe9324f6dc7bcc02449622636cc45ec04909b3120ab4df4726b21bf79e955fe8f832699d2196dcd7a58bfeafb170203010001a38186308183300f0603551d130101ff04053003020100300e0603551d0f0101ff0404030204b030200603551d250101ff0416301406082b0601050507030106082b06010505070302301d0603551d0e04160414643118e05209035edd83d44a0c368de2fb2fe4c0301f0603551d23041830168014643118e05209035edd83d44a0c368de2fb2fe4c0300d06092a864886f70d01010c05000382020100ad41c32bb52650eb4b76fce439c9404e84e4538a94916b3dc7983e8b5c58890556e7384601ca7440dde68233bb07b97bf879b64487b447df510897d2a0a4e789c409a9b237a6ad240ad5464f2ce80c58ddc4d07a29a74eb25e1223db6c00e334d7a27d32bfa6183a82f5e35bccf497c2445a526eabb0c068aba9b94cc092ea4756b0dcfb574f6179f0089e52b174ccdbd04123eeb6d70daeabd8513fcba6be0bc2b45ca9a69802dae11cc4d9ff6053b3a87fd8b0c6bf72fffc3b81167f73cca2b3fd656c5d353c8defca8a76e2ad535f984870a590af4e28fed5c5a125bf360747c5e7742e7813d1bd39b5498c8eb6ba72f267eda034314fdbc596f6b967a0ef8be5231d364e634444c84e64bd7919425171016fcd9bb05f01c58a303dee28241f6e860fc3aac3d92aad7dac2801ce79a3b41a0e1f1509fc0d86e96d94edb18616c000152490f64561713102128990fedd3a5fa642f2ff22dc11bc4dc5b209986a0c3e4eb2bdfdd40e9fdf246f702441cac058dd8d0d51eb0796e2bea2ce1b37b2a2f468505e1f8980a9f66d719df034a6fbbd2f9585991d259678fb9a4aebdc465d22c240351ed44abffbdd11b79a706fdf7c40158d3da87f68d7bd557191a8016b5b899c07bf1b87590feb4fa4203feea9a2a7a73ec224813a12b7a21e5dc93fcde4f0a7620f570d31fe27e9b8d65b74db7dc18a5e51adc42d7805d4661938"
37+
38+
39+
def setup_client():
40+
"""Initialize and set up the client with operator account"""
41+
load_dotenv()
42+
network = Network(network="solo")
43+
client = Client(network)
44+
45+
# Account 0.0.2 is a special administrative account with
46+
# elevated privileges for network management operations.
47+
# This account has the necessary permissions to create/delete/update nodes
48+
# The private key is intentionally public for local development.
49+
# Note: This setup only works on solo network and will not work on testnet/mainnet.
50+
original_operator_key = PrivateKey.from_string_der(
51+
"302e020100300506032b65700422042091132178e7"
52+
"2057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137"
53+
)
54+
client.set_operator(AccountId(0, 0, 2), original_operator_key)
55+
56+
return client
57+
58+
59+
def node_create():
60+
"""Demonstrates creating a node on the Hedera network."""
61+
client = setup_client()
62+
63+
# Node account ID - this should be an existing account
64+
# that will be associated with the node
65+
account_id = AccountId.from_string("0.0.4")
66+
67+
# Node description
68+
description = "Example node"
69+
70+
# Create endpoints for node services
71+
gossip_endpoint1 = Endpoint(domain_name="gossip1.example.com", port=50211)
72+
gossip_endpoint2 = Endpoint(domain_name="gossip2.example.com", port=50212)
73+
service_endpoint1 = Endpoint(domain_name="service1.example.com", port=50211)
74+
service_endpoint2 = Endpoint(domain_name="service2.example.com", port=50212)
75+
grpc_proxy_endpoint = Endpoint(domain_name="grpc.example.com", port=50213)
76+
77+
# Generate admin key for the node
78+
admin_key = PrivateKey.generate_ed25519()
79+
80+
# DER encoded x509 certificate
81+
gossip_ca_cert = bytes.fromhex(GOSSIP_CERTIFICATE)
82+
83+
# Create and execute the node creation transaction
84+
receipt = (
85+
NodeCreateTransaction()
86+
.set_account_id(account_id)
87+
.set_description(description)
88+
.set_gossip_endpoints([gossip_endpoint1, gossip_endpoint2])
89+
.set_service_endpoints([service_endpoint1, service_endpoint2])
90+
.set_gossip_ca_certificate(gossip_ca_cert)
91+
.set_admin_key(admin_key.public_key())
92+
.set_grpc_web_proxy_endpoint(grpc_proxy_endpoint)
93+
.set_decline_reward(True)
94+
.freeze_with(client)
95+
.sign(admin_key) # Sign with the admin key
96+
.execute(client)
97+
)
98+
99+
# Confirm successful execution
100+
if receipt.status != ResponseCode.SUCCESS:
101+
print(f"Node creation failed with status: {ResponseCode(receipt.status).name}")
102+
sys.exit(1)
103+
104+
print(f"Node created successfully with ID: {receipt.node_id}")
105+
106+
107+
if __name__ == "__main__":
108+
node_create()

src/hiero_sdk_python/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@
111111
from .schedule.schedule_create_transaction import ScheduleCreateTransaction
112112
from .schedule.schedule_id import ScheduleId
113113

114+
# Nodes
115+
from .nodes.node_create_transaction import NodeCreateTransaction
116+
114117
__all__ = [
115118
# Client
116119
"Client",
@@ -220,4 +223,7 @@
220223
# Schedule
221224
"ScheduleCreateTransaction",
222225
"ScheduleId",
226+
227+
# Nodes
228+
"NodeCreateTransaction",
223229
]

src/hiero_sdk_python/nodes/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)