From 44cbc0392d3d3957869df188ee938be723395367 Mon Sep 17 00:00:00 2001 From: K6-Rakshith Date: Sat, 9 Aug 2025 04:56:54 +0530 Subject: [PATCH] feat: enhance contributing guidelines and improve test coverage with detailed assertions --- CONTRIBUTING.md | 57 +++++++++++ Examples/CreateAccount/main.swift | 56 ++++++++++ Tests/HieroE2ETests/Batch.swift | 57 +++++------ Tests/HieroE2ETests/Client.swift | 34 +++++-- Tests/HieroE2ETests/Config.swift | 164 +++++++++++++----------------- 5 files changed, 232 insertions(+), 136 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..0f9dfbeb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing to hiero-sdk-swift + +Thank you for your interest in contributing! + +## How to Contribute + +1. **Fork the repository** and create your branch from `main`. +2. **Make your changes** in the appropriate folder: + - SDK code: `Sources/Hiero/` + - Example usage: `Examples/` + - Tests: `Tests/` +3. **Add or update documentation** as needed. +4. **Test your changes** to ensure nothing is broken. +5. **Commit and push** your branch. +6. **Open a Pull Request** on GitHub and describe your changes. + +## Local Setup + +1. Install [Swift](https://swift.org/download/) (5.7 or later recommended). +2. Clone the repository: + ```sh + git clone https://github.com/K6-Rakshith/hiero-sdk-swift.git + cd hiero-sdk-swift + ``` +3. Build the project: + ```sh + swift build + ``` +4. Run tests: + ```sh + swift test + ``` + +## Code Style +- Follow Swift best practices and existing code style. +- Add comments and documentation where helpful. +- Use descriptive variable and function names. + +## Commit Messages +- Use clear, concise commit messages. +- Reference issues or PRs when relevant (e.g., `Fixes #42`). + +## Pull Request Review Process +- All PRs are reviewed by maintainers. +- Address all review comments before merging. +- Ensure CI checks pass before requesting review. + +## Code of Conduct +- Be respectful and inclusive. +- See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) if available. + +## Reporting Issues +- Use GitHub Issues to report bugs or suggest features. +- Provide as much detail as possible (steps to reproduce, logs, etc.). + +## Questions? +- Check the README or open an issue. diff --git a/Examples/CreateAccount/main.swift b/Examples/CreateAccount/main.swift index 0c8c6a9b..c97b0f77 100644 --- a/Examples/CreateAccount/main.swift +++ b/Examples/CreateAccount/main.swift @@ -1,26 +1,80 @@ + +import Hiero +import SwiftDotenv + +@main +internal enum Program { + internal static func main() async throws { + let env = try Dotenv.load() + let client = try Client.forName(env.networkName) + + client.setOperator(env.operatorAccountId, env.operatorKey) + + let newKey = PrivateKey.generateEd25519() + + print("private key = \(newKey)") + print("public key = \(newKey.publicKey)") + + let response = try await AccountCreateTransaction() + .keyWithoutAlias(.single(newKey.publicKey)) + .initialBalance(5) + .execute(client) + + let receipt = try await response.getReceipt(client) + let newAccountId = receipt.accountId! + + print("account address = \(newAccountId)") + } +} + +extension Environment { + /// Account ID for the operator to use in this example. + internal var operatorAccountId: AccountId { + AccountId(self["OPERATOR_ID"]!.stringValue)! + } + + /// Private key for the operator to use in this example. + internal var operatorKey: PrivateKey { + PrivateKey(self["OPERATOR_KEY"]!.stringValue)! + } // SPDX-License-Identifier: Apache-2.0 import Hiero import SwiftDotenv +/// Example program to create a new Hedera account using the Hiero SDK. +/// +/// This script demonstrates: +/// 1. Loading environment variables for network and operator credentials. +/// 2. Initializing a client for the target network. +/// 3. Generating a new Ed25519 key pair. +/// 4. Creating a new account with the generated public key and an initial balance. +/// 5. Printing the new account's ID and the generated keys. @main internal enum Program { internal static func main() async throws { + // Load environment variables from .env file let env = try Dotenv.load() + + // Initialize the client for the specified network let client = try Client.forName(env.networkName) + // Set the operator (payer) for transactions client.setOperator(env.operatorAccountId, env.operatorKey) + // Generate a new Ed25519 private/public key pair let newKey = PrivateKey.generateEd25519() print("private key = \(newKey)") print("public key = \(newKey.publicKey)") + // Create a new account with the generated public key and initial balance of 5 hbars let response = try await AccountCreateTransaction() .keyWithoutAlias(.single(newKey.publicKey)) .initialBalance(5) .execute(client) + // Get the receipt and extract the new account ID let receipt = try await response.getReceipt(client) let newAccountId = receipt.accountId! @@ -30,11 +84,13 @@ internal enum Program { extension Environment { /// Account ID for the operator to use in this example. + /// Reads from the OPERATOR_ID environment variable. internal var operatorAccountId: AccountId { AccountId(self["OPERATOR_ID"]!.stringValue)! } /// Private key for the operator to use in this example. + /// Reads from the OPERATOR_KEY environment variable. internal var operatorKey: PrivateKey { PrivateKey(self["OPERATOR_KEY"]!.stringValue)! } diff --git a/Tests/HieroE2ETests/Batch.swift b/Tests/HieroE2ETests/Batch.swift index 1b005fae..4b4d19bf 100644 --- a/Tests/HieroE2ETests/Batch.swift +++ b/Tests/HieroE2ETests/Batch.swift @@ -3,7 +3,7 @@ import Hiero import XCTest -internal final class Batch: XCTestCase { +internal final class BatchTests: XCTestCase { internal func testBatchOneTransaction() async throws { let testEnv = try TestEnvironment.nonFree @@ -19,9 +19,13 @@ internal final class Batch: XCTestCase { .addInnerTransaction(accountCreateTx) .freezeWith(testEnv.client) .sign(batchKey) - _ = try await batchTx.execute(testEnv.client).getReceipt(testEnv.client) + let batchReceipt = try await batchTx.execute(testEnv.client).getReceipt(testEnv.client) + + XCTAssertEqual(batchReceipt.status, .success) let transactionIds = batchTx.innerTransactionIds + XCTAssertEqual(transactionIds.count, 1) + let accountCreateReceipt = try await TransactionReceiptQuery() .transactionId(transactionIds[0]).execute(testEnv.client) let accountId = try XCTUnwrap(accountCreateReceipt.accountId) @@ -44,38 +48,21 @@ internal final class Batch: XCTestCase { .batchify(client: testEnv.client, .single(batchKey.publicKey)) .freezeWith(testEnv.client) - let batchTx = try BatchTransaction() - /// 25 account create transactions - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) - .addInnerTransaction(accountCreateTx) + var batchTx = BatchTransaction() + for _ in 0..<25 { + try batchTx.addInnerTransaction(accountCreateTx) + } + + batchTx = try batchTx .freezeWith(testEnv.client) .sign(batchKey) - _ = try await batchTx.execute(testEnv.client).getReceipt(testEnv.client) + let batchReceipt = try await batchTx.execute(testEnv.client).getReceipt(testEnv.client) + + XCTAssertEqual(batchReceipt.status, .success) let transactionIds = batchTx.innerTransactionIds + XCTAssertEqual(transactionIds.count, 25) + let accountCreateReceipt = try await TransactionReceiptQuery() .transactionId(transactionIds[0]).execute(testEnv.client) let accountId = try XCTUnwrap(accountCreateReceipt.accountId) @@ -137,7 +124,9 @@ internal final class Batch: XCTestCase { .addInnerTransaction(topicMsgSubmitTx) .freezeWith(testEnv.client) .sign(batchKey) - _ = try await batchTx.execute(testEnv.client).getReceipt(testEnv.client) + let batchReceipt = try await batchTx.execute(testEnv.client).getReceipt(testEnv.client) + + XCTAssertEqual(batchReceipt.status, .success) let info = try await TopicInfoQuery(topicId: topicId).execute(testEnv.client) @@ -251,9 +240,9 @@ internal final class Batch: XCTestCase { let finalOperatorBalance = try await AccountBalanceQuery().accountId(testEnv.operator.accountId).execute( testEnv.client - ) - .hbars - XCTAssertLessThan(initialOperatorBalance, finalOperatorBalance) + ).hbars + + XCTAssertLessThan(finalOperatorBalance, initialOperatorBalance) } internal func testBatchifiedTxButNotInBatch() async throws { diff --git a/Tests/HieroE2ETests/Client.swift b/Tests/HieroE2ETests/Client.swift index 7981401c..6355eca0 100644 --- a/Tests/HieroE2ETests/Client.swift +++ b/Tests/HieroE2ETests/Client.swift @@ -3,14 +3,36 @@ import Hiero import XCTest -internal class ClientIntegrationTests: XCTestCase { +internal final class ClientIntegrationTests: XCTestCase { internal func testInitWithMirrorNetwork() async throws { - let mirrorNetworkString = "testnet.mirrornode.hedera.com:443" - let client = try await Client.forMirrorNetwork([mirrorNetworkString]) + // Define the mirror network address for testnet + let mirrorNetworkAddress = "testnet.mirrornode.hedera.com:443" + + // Initialize the client with the mirror network + let client: Client + do { + client = try await Client.forMirrorNetwork([mirrorNetworkAddress]) + } catch { + XCTFail("Client initialization failed with error: \(error.localizedDescription)") + throw error + } + + // Verify the mirror network is set correctly let mirrorNetwork = client.mirrorNetwork + XCTAssertEqual(mirrorNetwork.count, 1, "Mirror network should contain exactly one address") + XCTAssertEqual(mirrorNetwork[0], mirrorNetworkAddress, "Mirror network address mismatch") + + // Verify the main network is initialized and not nil/empty + XCTAssertNotNil(client.network, "Main network should be initialized") + XCTAssertFalse(client.network.isEmpty, "Main network should not be empty after mirror initialization") + + // Optional: Perform a simple query to validate client functionality (e.g., get network name or a basic info) + // This ensures the client is not just created but usable; adjust based on SDK capabilities + XCTAssertNoThrow(try await client.pingAll(), "Client should be able to ping the network") - XCTAssertEqual(mirrorNetwork.count, 1) - XCTAssertEqual(mirrorNetwork[0], mirrorNetworkString) - XCTAssertNotNil(client.network) + // Teardown: Close the client to release resources (if supported by SDK) + addTeardownBlock { + client.close() + } } } diff --git a/Tests/HieroE2ETests/Config.swift b/Tests/HieroE2ETests/Config.swift index a6ee0084..336b4698 100644 --- a/Tests/HieroE2ETests/Config.swift +++ b/Tests/HieroE2ETests/Config.swift @@ -4,146 +4,122 @@ import Hiero import SwiftDotenv import XCTest +/// A simple bucket-based ratelimiter to prevent overloading the network during tests. private struct Bucket { - /// Divide the entire ratelimit for everything by this amount because we don't actually want to use the entire network's worth of rss. + /// Divide the entire ratelimit for everything by this amount to avoid using the full network capacity. private static let globalDivider: Int = 2 - /// Multiply the refresh delay + /// Multiply the refresh delay for safety margin. private static let refreshMultiplier: Double = 1.05 /// Create a bucket for at most `limit` items per `refreshDelay`. internal init(limit: Int, refreshDelay: TimeInterval) { - precondition(limit > 0) self.limit = max(limit / Self.globalDivider, 1) self.refreshDelay = refreshDelay * Self.refreshMultiplier self.items = [] } fileprivate var limit: Int - // how quickly items are removed (an item older than `refreshDelay` is dropped) + // How quickly items are removed (an item older than `refreshDelay` is dropped). fileprivate var refreshDelay: TimeInterval fileprivate var items: [Date] - fileprivate mutating func next(now: Date = Date()) -> UInt64? { + fileprivate mutating func next(now: Date = Date()) -> TimeInterval? { items.removeAll { now.timeIntervalSince($0) >= refreshDelay } - let usedTime: Date - - if items.count >= limit { - // if the limit is `2` items per `0.5` seconds and we have `3` items, we want `items[1] + 0.5 seconds` - // because `items[1]` will expire 0.5 seconds after *it* was added. - usedTime = items[items.count - limit] + refreshDelay - } else { - usedTime = now + guard items.count >= limit else { + items.append(now) + return nil } - items.append(usedTime) + // Calculate the time when the next slot opens. + let usedTime = items[items.count - limit] + refreshDelay - if usedTime > now { - return UInt64(usedTime.timeIntervalSince(now) * 1e9) - } + items.append(usedTime) - return nil + return max(0, usedTime.timeIntervalSince(now)) } } -/// Ratelimits for the really stringent operations. -/// -/// This is a best-effort attempt to protect against E2E tests being flakey due to Hedera having a global ratelimit per transaction type. +/// Ratelimits for stringent operations to avoid flakiness in E2E tests due to global limits. internal actor Ratelimit { - // todo: use something fancier or find something fancier, preferably the latter, but the swift ecosystem is as it is. private var accountCreate = Bucket(limit: 2, refreshDelay: 1.0) private var file = Bucket(limit: 10, refreshDelay: 1.0) - // private var topicCreate = Bucket(limit: 5, refreshDelay: 1.0) + // Add more buckets as needed, e.g., private var topicCreate = Bucket(limit: 5, refreshDelay: 1.0) internal func accountCreate() async throws { - // if let sleepTime = accountCreate.next() { - // try await Task.sleep(nanoseconds: sleepTime) - // } + if let sleepTime = accountCreate.next() { + try await Task.sleep(nanoseconds: UInt64(sleepTime * 1e9)) + } } internal func file() async throws { if let sleepTime = file.next() { - try await Task.sleep(nanoseconds: sleepTime) + try await Task.sleep(nanoseconds: UInt64(sleepTime * 1e9)) } } } +/// Shared test environment configuration and utilities. internal struct TestEnvironment { private let defaultLocalNodeAddress: String = "127.0.0.1:50211" private let defaultLocalMirrorNodeAddress: String = "127.0.0.1:5600" - internal struct Config { - private static func envBool(env: Environment?, key: String, defaultValue: Bool) -> Bool { - guard let value = env?[key]?.stringValue else { - return defaultValue - } - - switch value { - case "1": - return true - case "0": - return false - case _: - print( - "warn: expected `\(key)` to be `1` or `0` but it was `\(value)`, returning `\(defaultValue)`", - stderr - ) + internal struct Config { + private static func parseBool(from value: String?, defaultValue: Bool) -> Bool { + guard let value = value else { return defaultValue } + switch value.lowercased() { + case "1", "true", "yes": return true + case "0", "false", "no": return false + default: + print("Warning: Invalid boolean value '\(value)' for key; using default \(defaultValue)") return defaultValue } } fileprivate init() { - let env = try? Dotenv.load() - - network = env?[Keys.network]?.stringValue ?? "testnet" - - let runNonfree = Self.envBool(env: env, key: Keys.runNonfree, defaultValue: false) + guard let env = try? Dotenv.load() else { + print("Warning: Failed to load .env file; using defaults") + self.network = "testnet" + self.operator = nil + self.runNonfreeTests = false + return + } - `operator` = .init(env: env) + self.network = env[Keys.network]?.stringValue ?? "testnet" + self.runNonfreeTests = Self.parseBool(from: env[Keys.runNonfree]?.stringValue, defaultValue: false) - if `operator` == nil && runNonfree { - print("warn: forcing `runNonfree` to false because operator is nil", stderr) - self.runNonfreeTests = false + if let op = Operator(env: env) { + self.operator = op } else { - self.runNonfreeTests = runNonfree + self.operator = nil + if runNonfreeTests { + print("Warning: Disabling non-free tests due to missing operator config") + self.runNonfreeTests = false + } } } internal let network: String - internal let `operator`: TestEnvironment.Operator? - internal let runNonfreeTests: Bool + internal let operator: TestEnvironment.Operator? + internal var runNonfreeTests: Bool } internal struct Operator { - internal init?(env: Environment?) { - guard let env = env else { + internal init?(env: Environment) { + guard let keyStr = env[Keys.operatorKey]?.stringValue, + let accountIdStr = env[Keys.operatorAccountId]?.stringValue + else { return nil } - let operatorKeyStr = env[Keys.operatorKey]?.stringValue - let operatorAccountIdStr = env[Keys.operatorAccountId]?.stringValue - - switch (operatorKeyStr, operatorAccountIdStr) { - case (nil, nil): - return nil - - case (.some, nil), (nil, .some): - - // warn: + do { + let accountId = try AccountId.fromString(accountIdStr) + let key = try PrivateKey.fromString(keyStr) + self.accountId = accountId + self.privateKey = key + } catch { + print("Warning: Invalid operator config: \(error)") return nil - - case (.some(let key), .some(let accountId)): - - do { - let accountId = try AccountId.fromString(accountId) - let key = try PrivateKey.fromString(key) - - self.accountId = accountId - self.privateKey = key - } catch { - print("warn: forcing operator to nil because an error occurred: \(error)") - return nil - } } } @@ -171,19 +147,16 @@ internal struct TestEnvironment { case "previewnet": self.client = Client.forPreviewnet() case "localhost": - var network: [String: AccountId] = [String: AccountId]() - + var network: [String: AccountId] = [:] network[defaultLocalNodeAddress] = AccountId(num: 3) - let client = try Client.forNetwork(network) - self.client = client.setMirrorNetwork([defaultLocalMirrorNodeAddress]) default: + print("Warning: Unknown network '\(config.network)'; defaulting to testnet") self.client = Client.forTestnet() } } catch { - print("Error creating client: \(config.network); creating one using testnet") - + print("Error initializing client for \(config.network): \(error); defaulting to testnet") self.client = Client.forTestnet() } @@ -192,14 +165,14 @@ internal struct TestEnvironment { } } - internal static let global: TestEnvironment = TestEnvironment() + internal static let shared: TestEnvironment = TestEnvironment() internal static var nonFree: NonfreeTestEnvironment { get throws { - if let inner = NonfreeTestEnvironment.global { + if let inner = NonfreeTestEnvironment(shared) { return inner } - throw XCTSkip("Test requires non-free test environment, but the test environment only allows free tests") + throw XCTSkip("Test requires non-free environment, but only free tests are enabled") } } @@ -207,7 +180,7 @@ internal struct TestEnvironment { internal let config: Config internal let ratelimits: Ratelimit - internal var `operator`: Operator? { + internal var operator: Operator? { config.operator } } @@ -215,16 +188,15 @@ internal struct TestEnvironment { internal struct NonfreeTestEnvironment { internal struct Config { fileprivate init?(base: TestEnvironment.Config) { - guard base.runNonfreeTests else { + guard base.runNonfreeTests, let op = base.operator else { return nil } - - self.operator = base.`operator`! self.network = base.network + self.operator = op } internal let network: String - internal let `operator`: TestEnvironment.Operator + internal let operator: TestEnvironment.Operator } private init?(_ env: TestEnvironment) { @@ -237,13 +209,13 @@ internal struct NonfreeTestEnvironment { self.ratelimits = env.ratelimits } - fileprivate static let global: Self? = Self(.global) + fileprivate static let shared: Self? = Self(.shared) internal let client: Hiero.Client internal let config: Config internal let ratelimits: Ratelimit - internal var `operator`: TestEnvironment.Operator { + internal var operator: TestEnvironment.Operator { config.operator } }