Skip to content

Commit ff260ad

Browse files
authored
HIP-1046 & HIP-1064 (#492)
* feat: add grpcWebProxyEndpoint fields to node create and update and add unit tests Signed-off-by: Rob Walworth <[email protected]> * feat: trying to get example to work Signed-off-by: Rob Walworth <[email protected]> * feat: testing 1046 and 1064 WIP Signed-off-by: Rob Walworth <[email protected]> * it works Signed-off-by: Rob Walworth <[email protected]> * trying things Signed-off-by: Rob Walworth <[email protected]> * chore: it works let's goooo Signed-off-by: Rob Walworth <[email protected]> * refactor: organize solo parsing functionality Signed-off-by: Rob Walworth <[email protected]> * refactor: format Signed-off-by: Rob Walworth <[email protected]> --------- Signed-off-by: Rob Walworth <[email protected]>
1 parent 55e9347 commit ff260ad

File tree

11 files changed

+621
-5
lines changed

11 files changed

+621
-5
lines changed
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
import Foundation
4+
import Hiero
5+
import HieroExampleUtilities
6+
import Network
7+
import SwiftDotenv
8+
9+
// Set the paths **HERE** for the json files provided by solo.
10+
let nodeCreateConfigPath = "add/path/here"
11+
let nodeUpdateConfigPath = "add/path/here"
12+
13+
@main
14+
internal enum Program {
15+
internal static func main() async throws {
16+
let env = try Dotenv.load()
17+
let client = try Client.forName(env.networkName)
18+
19+
// Set operator account
20+
client.setOperator(env.operatorAccountId, env.operatorKey)
21+
await client.setNetworkUpdatePeriod(nanoseconds: 1_000_000_000 * 1_000_000)
22+
23+
// Before running this example, you should already have solo up and running,
24+
// as well as having run an `add-prepare` command, to ready solo for the new
25+
// node to be added as well as being provided with needed values for the
26+
// NodeCreateTransaction you will send to solo.
27+
28+
// Load the node create configuration from solo.
29+
let cfg = try loadNodeCreateConfig(at: nodeCreateConfigPath)
30+
31+
// Map JSON -> SDK types
32+
let accountId = try AccountId.fromString(cfg.newNode.accountId)
33+
34+
// Endpoints
35+
let gossipEndpoints: [Endpoint] = try cfg.gossipEndpoints.map(endpointFrom)
36+
let grpcServiceEndpoints: [Endpoint] = try cfg.grpcServiceEndpoints.map(endpointFrom)
37+
38+
// Certificates / hashes
39+
let signingCertDer = try dataFromCommaSeparatedBytes(cfg.signingCertDer)
40+
let tlsCertHash = Data(cfg.tlsCertHash.data)
41+
42+
// Admin key
43+
let adminKey = try PrivateKey.fromString(cfg.adminKey)
44+
45+
// Create the node
46+
print("Creating a new node\(cfg.newNode.name.map { " named \($0)" } ?? "") from \(nodeCreateConfigPath)...")
47+
48+
let createTransaction = try NodeCreateTransaction()
49+
.accountId(accountId)
50+
.gossipEndpoints(gossipEndpoints)
51+
.serviceEndpoints(grpcServiceEndpoints)
52+
.gossipCaCertificate(signingCertDer)
53+
.grpcCertificateHash(tlsCertHash)
54+
.grpcWebProxyEndpoint(grpcServiceEndpoints[0])
55+
.adminKey(.single(adminKey.publicKey))
56+
.freezeWith(client)
57+
.sign(adminKey)
58+
59+
let response = try await createTransaction.execute(client)
60+
let receipt = try await response.getReceipt(client)
61+
print("Node create receipt: \(receipt)")
62+
63+
// Allow yourself five minutes to freeze solo and restart it with an `add-execute`.
64+
try await Task.sleep(nanoseconds: 1_000_000_000 * 60 * 5)
65+
66+
// Print off the address book to verify the creation of the new node.
67+
let addressBook = try await NodeAddressBookQuery().setFileId(FileId.addressBook).execute(client)
68+
for nodeAddress in addressBook.nodeAddresses {
69+
print("Node ID: \(nodeAddress.nodeId)")
70+
print("Node Account ID: \(nodeAddress.nodeAccountId)")
71+
print("Node Description: \(nodeAddress.description)")
72+
}
73+
74+
// Load the node update configuration from solo.
75+
let updateCfg = try loadNodeUpdateConfig(at: nodeUpdateConfigPath)
76+
let updateInputs = try makeNodeUpdateInputs(from: updateCfg)
77+
78+
// 2. Update the node
79+
print("Updating the node...")
80+
let updateTransaction = try NodeUpdateTransaction()
81+
.nodeId(1)
82+
.declineRewards(true)
83+
.freezeWith(client)
84+
.sign(updateInputs.adminKey)
85+
let updateTransactionResponse = try await updateTransaction.execute(client)
86+
let updateTransactionReceipt = try await updateTransactionResponse.getReceipt(client)
87+
print("Node update receipt: \(updateTransactionReceipt)")
88+
89+
// Allow yourself five minutes to freeze solo and restart it with an `update-execute`.
90+
try await Task.sleep(nanoseconds: 1_000_000_000 * 60 * 5)
91+
92+
// Print off the address book to verify the update of the node.
93+
let addressBook2 = try await NodeAddressBookQuery().setFileId(FileId.addressBook).execute(client)
94+
for nodeAddress in addressBook2.nodeAddresses {
95+
print("Node ID: \(nodeAddress.nodeId)")
96+
print("Node Account ID: \(nodeAddress.nodeAccountId)")
97+
print("Node Description: \(nodeAddress.description)")
98+
}
99+
}
100+
}
101+
102+
extension Environment {
103+
/// Account ID for the operator to use in this example.
104+
internal var operatorAccountId: AccountId {
105+
AccountId(self["OPERATOR_ID"]!.stringValue)!
106+
}
107+
108+
/// Private key for the operator to use in this example.
109+
internal var operatorKey: PrivateKey {
110+
PrivateKey(self["OPERATOR_KEY"]!.stringValue)!
111+
}
112+
113+
/// The name of the hedera network this example should be ran against.
114+
///
115+
/// Testnet by default.
116+
internal var networkName: String {
117+
self["HEDERA_NETWORK"]?.stringValue ?? "testnet"
118+
}
119+
}
120+
121+
extension Data {
122+
internal init?<S: StringProtocol>(hexEncoded: S) {
123+
let chars = Array(hexEncoded.utf8)
124+
// note: hex check is done character by character
125+
let count = chars.count
126+
127+
guard count % 2 == 0 else {
128+
return nil
129+
}
130+
131+
var arr: [UInt8] = Array()
132+
arr.reserveCapacity(count / 2)
133+
134+
for idx in stride(from: 0, to: hexEncoded.count, by: 2) {
135+
// swiftlint complains about the length of these if they're less than 4 characters
136+
// that'd be fine and all, but `low` is still only 3 characters.
137+
guard let highNibble = hexVal(UInt8(chars[idx])), let lowNibble = hexVal(UInt8(chars[idx + 1])) else {
138+
return nil
139+
}
140+
141+
arr.append(highNibble << 4 | lowNibble)
142+
}
143+
144+
self.init(arr)
145+
}
146+
}
147+
148+
private func hexVal(_ char: UInt8) -> UInt8? {
149+
// this would be a very clean function if swift had a way of doing ascii-charcter literals, but it can't.
150+
let ascii0: UInt8 = 0x30
151+
let ascii9: UInt8 = ascii0 + 9
152+
let asciiUppercaseA: UInt8 = 0x41
153+
let asciiUppercaseF: UInt8 = 0x46
154+
let asciiLowercaseA: UInt8 = asciiUppercaseA | 0x20
155+
let asciiLowercaseF: UInt8 = asciiUppercaseF | 0x20
156+
switch char {
157+
case ascii0...ascii9:
158+
return char - ascii0
159+
case asciiUppercaseA...asciiUppercaseF:
160+
return char - asciiUppercaseA + 10
161+
case asciiLowercaseA...asciiLowercaseF:
162+
return char - asciiLowercaseA + 10
163+
default:
164+
return nil
165+
}
166+
}
167+
168+
func byteStringToPEMData(_ byteString: String, label: String = "CERTIFICATE") -> Data? {
169+
// Step 1: Convert comma-separated decimal string to Data
170+
let components =
171+
byteString
172+
.split(separator: ",")
173+
.map { $0.trimmingCharacters(in: .whitespaces) }
174+
175+
var rawData = Data(capacity: components.count)
176+
for byteStr in components {
177+
guard let byte = UInt8(byteStr) else {
178+
print("Invalid byte value: \(byteStr)")
179+
return nil
180+
}
181+
rawData.append(byte)
182+
}
183+
184+
// Step 2: Base64 encode the raw data
185+
let base64String = rawData.base64EncodedString()
186+
187+
// Step 3: Split base64 string into 64-character lines
188+
var pemBody = ""
189+
var index = base64String.startIndex
190+
while index < base64String.endIndex {
191+
let nextIndex =
192+
base64String.index(index, offsetBy: 64, limitedBy: base64String.endIndex) ?? base64String.endIndex
193+
pemBody += base64String[index..<nextIndex] + "\n"
194+
index = nextIndex
195+
}
196+
197+
// Step 4: Add PEM headers
198+
let pemString = "-----BEGIN \(label)-----\n" + pemBody + "-----END \(label)-----\n"
199+
return pemString.data(using: .utf8)
200+
}
201+
202+
func byteStringToData(_ byteString: String) -> Data? {
203+
let components =
204+
byteString
205+
.split(separator: ",")
206+
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
207+
208+
var data = Data(capacity: components.count)
209+
210+
for byteStr in components {
211+
guard let byte = UInt8(byteStr) else {
212+
print("Invalid byte value: \(byteStr)")
213+
return nil
214+
}
215+
data.append(byte)
216+
}
217+
218+
return data
219+
}
220+
221+
func byteArrayToData(_ bytes: [Int]) -> Data? {
222+
var data = Data(capacity: bytes.count)
223+
224+
for byte in bytes {
225+
guard (0...255).contains(byte) else {
226+
print("Invalid byte value: \(byte). Must be in 0...255.")
227+
return nil
228+
}
229+
data.append(UInt8(byte))
230+
}
231+
232+
return data
233+
}
234+
235+
// A convenient, typed bundle to use downstream.
236+
private struct NodeUpdateInputs {
237+
let adminKey: PrivateKey
238+
let newAdminKey: PrivateKey?
239+
let freezeAdminPrivateKey: PrivateKey
240+
let treasuryKey: PrivateKey
241+
let nodeAlias: String
242+
let existingNodeAliases: [String]
243+
let allNodeAliases: [String]
244+
let upgradeZipHash: Data?
245+
let newAccountId: AccountId?
246+
// Keep these raw until we know format/usage
247+
let tlsPublicKey: String?
248+
let tlsPrivateKey: String?
249+
let gossipPublicKey: String?
250+
let gossipPrivateKey: String?
251+
}
252+
253+
/// Build a Hiero `Endpoint` from host:port string
254+
private func endpointFrom(_ s: String) throws -> Endpoint {
255+
let (host, port) = try parseHostPort(s)
256+
guard let ip = IPv4Address(host) else {
257+
// Domain name
258+
return Endpoint(port: port, domainName: host)
259+
}
260+
// IP address: keep domain blank like your original
261+
return Endpoint(ipAddress: ip, port: port, domainName: "")
262+
}
263+
264+
/// Parse PrivateKey from a maybe-blank string.
265+
private func parsePrivateKey(_ s: String?, field: String) throws -> PrivateKey? {
266+
guard let s = nilIfBlank(s) else { return nil }
267+
do { return try PrivateKey.fromString(s) } catch {
268+
throw NSError(domain: "Config", code: 30, userInfo: [NSLocalizedDescriptionKey: "Invalid \(field): \(error)"])
269+
}
270+
}
271+
272+
/// Parse AccountId from either "0.0.123" or just "123".
273+
private func parseAccountIdFlexible(_ s: String?) throws -> AccountId? {
274+
guard let s = nilIfBlank(s) else { return nil }
275+
// If it already looks like 0.0.x, defer to SDK.
276+
if s.contains(".") {
277+
return try AccountId.fromString(s)
278+
}
279+
// Otherwise treat as numeric 'num' in 0.0.num
280+
guard let num = UInt64(s) else {
281+
throw NSError(
282+
domain: "Config", code: 40,
283+
userInfo: [NSLocalizedDescriptionKey: "newAccountNumber must be a uint or '0.0.x'"])
284+
}
285+
return try AccountId.fromString("0.0.\(num)")
286+
}
287+
288+
// MARK: - Map JSON -> strongly typed inputs
289+
290+
private func makeNodeUpdateInputs(from cfg: NodeUpdateConfig) throws -> NodeUpdateInputs {
291+
let adminKey = try parsePrivateKey(cfg.adminKey, field: "adminKey")!
292+
let newAdminKey = try parsePrivateKey(cfg.newAdminKey, field: "newAdminKey")
293+
let freezeAdmin = try parsePrivateKey(cfg.freezeAdminPrivateKey, field: "freezeAdminPrivateKey")!
294+
let treasuryKey = try parsePrivateKey(cfg.treasuryKey, field: "treasuryKey")!
295+
296+
let upgradeZipHash = try dataFromHex(cfg.upgradeZipHash)
297+
let newAccountId = try parseAccountIdFlexible(cfg.newAccountNumber)
298+
299+
return NodeUpdateInputs(
300+
adminKey: adminKey,
301+
newAdminKey: newAdminKey,
302+
freezeAdminPrivateKey: freezeAdmin,
303+
treasuryKey: treasuryKey,
304+
nodeAlias: cfg.nodeAlias,
305+
existingNodeAliases: cfg.existingNodeAliases ?? [],
306+
allNodeAliases: cfg.allNodeAliases ?? [],
307+
upgradeZipHash: upgradeZipHash,
308+
newAccountId: newAccountId,
309+
tlsPublicKey: nilIfBlank(cfg.tlsPublicKey),
310+
tlsPrivateKey: nilIfBlank(cfg.tlsPrivateKey),
311+
gossipPublicKey: nilIfBlank(cfg.gossipPublicKey),
312+
gossipPrivateKey: nilIfBlank(cfg.gossipPrivateKey)
313+
)
314+
}

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ let exampleTargets = [
3838
"CreateStatefulContract",
3939
"CreateTopic",
4040
"CreateTopicWithRevenue",
41+
"CreateUpdateDeleteNode",
4142
"DeleteAccount",
4243
"DeleteFile",
4344
"FileAppendChunked",

0 commit comments

Comments
 (0)