Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 77 additions & 2 deletions .github/workflows/swift-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,90 @@ jobs:

- name: Prepare Hiero Solo
id: solo
uses: hiero-ledger/hiero-solo-action@6a1a77601cf3e69661fb6880530a4edf656b40d5 # v0.14.0
uses: hiero-ledger/hiero-solo-action@fbca3e7a99ce9aa8a250563a81187abe115e0dad # v0.16.0
with:
installMirrorNode: true
mirrorNodeVersion: v0.142.0
hieroVersion: v0.68.0
hieroVersion: v0.68.4

- name: Run integration tests (HieroIntegrationTests)
env:
HIERO_PROFILE: ciIntegration
HIERO_OPERATOR_ID: 0.0.2
HIERO_OPERATOR_KEY: 302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137
run: swift test --filter HieroIntegrationTests

test-dab:
name: Build and Test SDK with DAB
strategy:
matrix:
swift: ["5.9", "5.10"]
runs-on: hiero-client-sdk-linux-large
env:
SOLO_CLUSTER_NAME: solo
SOLO_NAMESPACE: solo
SOLO_CLUSTER_SETUP_NAMESPACE: solo-cluster
SOLO_DEPLOYMENT: solo-deployment
KIND_IMAGE: kindest/node:v1.32.2 # pin to a stable image

steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f
with:
egress-policy: audit

- name: Setup Swift
uses: SwiftyLab/setup-swift@4bbb093f8c68d1dee1caa8b67c681a3f8fe70a91 # v1.12.0
with:
swift-version: ${{ matrix.swift }}

- name: Checkout Code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

- name: Cache SPM build dir
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57
with:
path: .build
key: ${{ runner.os }}-${{ matrix.swift }}-spm-${{ github.job }}-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-${{ matrix.swift }}-spm-

- name: Install system dependencies for Swift/gRPC
run: |
set -euxo pipefail
sudo apt-get update
sudo apt-get install -y \
git curl jq coreutils ca-certificates \
clang libicu-dev libxml2-dev libsqlite3-dev zlib1g-dev \
libcurl4-openssl-dev libssl-dev pkg-config \
protobuf-compiler

- name: Verify Swift installation
run: |
swift --version
which swift
swift package --version

- name: Build SDK
run: |
swift build

- name: Prepare Hiero Solo
id: solo
uses: hiero-ledger/hiero-solo-action@fbca3e7a99ce9aa8a250563a81187abe115e0dad # v0.16.0
with:
installMirrorNode: true
hieroVersion: v0.68.4
mirrorNodeVersion: v0.142.0
dualMode: true

- name: Run DAB-related integration tests
env:
HIERO_PROFILE: ciIntegration
HIERO_OPERATOR_ID: 0.0.2
HIERO_OPERATOR_KEY: 302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137
HIERO_ENVIRONMENT_TYPE: custom
HIERO_CONSENSUS_NODES: 127.0.0.1:50211,127.0.0.1:51211
HIERO_CONSENSUS_NODE_ACCOUNT_IDS: 0.0.3,0.0.4
HIERO_MIRROR_NODES: 127.0.0.1:5600
run: swift test --filter "DAB*"
33 changes: 30 additions & 3 deletions Sources/Hiero/Client/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ public final class Client: Sendable {
/// Realm number for this client's network (backing storage)
private let _realm: UInt64

/// Flag to indicate if this client should use only plaintext endpoints (for integration testing)
private let _plaintextOnly: Bool

// MARK: - Initialization

/// Primary designated initializer.
Expand All @@ -85,7 +88,8 @@ public final class Client: Sendable {
networkUpdatePeriod: UInt64? = TimeInterval(86400).nanoseconds, // 24 hours
_ eventLoop: NIOCore.EventLoopGroup,
shard: UInt64 = 0,
realm: UInt64 = 0
realm: UInt64 = 0,
plaintextOnly: Bool = false
) {
self.eventLoop = eventLoop
self._consensusNetwork = .init(consensus)
Expand All @@ -101,12 +105,14 @@ public final class Client: Sendable {
mirrorNetwork: _mirrorNetwork,
updatePeriod: networkUpdatePeriod,
shard: shard,
realm: realm
realm: realm,
plaintext: plaintextOnly
)
self._networkUpdatePeriod = .init(networkUpdatePeriod)
self._backoff = .init(Backoff())
self._shard = shard
self._realm = realm
self._plaintextOnly = plaintextOnly
}

// MARK: - Internal Accessors
Expand Down Expand Up @@ -137,6 +143,26 @@ public final class Client: Sendable {
return .fromTinybars(value)
}

/// Whether this client should use only plaintext endpoints.
internal var plaintextOnly: Bool {
_plaintextOnly
}

/// Shard number for this client's network (internal accessor)
internal var shard: UInt64 {
_shard
}

/// Realm number for this client's network (internal accessor)
internal var realm: UInt64 {
_realm
}

/// Internal accessor for the actual MirrorNetwork object (not just addresses)
internal var mirrorNetworkObject: MirrorNetwork {
_mirrorNetwork.load(ordering: .relaxed)
}

// MARK: - Public Accessors

/// Returns the shard number for this client's network.
Expand Down Expand Up @@ -246,7 +272,8 @@ public final class Client: Sendable {
ledgerId: nil,
eventLoop,
shard: shard,
realm: realm
realm: realm,
plaintextOnly: true // forMirrorNetwork always uses plaintext endpoints
)

let addressBook = try await NodeAddressBookQuery()
Expand Down
8 changes: 8 additions & 0 deletions Sources/Hiero/Client/ConsensusNetwork.swift
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,14 @@ internal final class ConsensusNetwork: Sendable, AtomicReference {
}
}

/// Converts account IDs to their corresponding node indexes, skipping unknown account IDs.
///
/// - Parameter accountIds: Array of account IDs to look up
/// - Returns: Array of node indexes for known account IDs only
internal func nodeIndexesForIdsAllowingUnknown(_ accountIds: [AccountId]) -> [Int] {
accountIds.compactMap { nodeIndexMap[$0] }
}

/// Returns indexes of all currently healthy nodes.
internal func healthyNodeIndexes() -> [Int] {
let now = Timestamp.now
Expand Down
47 changes: 38 additions & 9 deletions Sources/Hiero/Client/NetworkUpdateTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ internal actor NetworkUpdateTask {
/// Atomic reference to the mirror network for address book queries
private let mirrorNetwork: ManagedAtomic<MirrorNetwork>

/// Whether to use only plaintext endpoints
private let plaintext: Bool

/// The background task performing periodic updates
private var updateTask: Task<(), Error>?

Expand All @@ -63,11 +66,13 @@ internal actor NetworkUpdateTask {
mirrorNetwork: ManagedAtomic<MirrorNetwork>,
updatePeriod: UInt64?,
shard: UInt64,
realm: UInt64
realm: UInt64,
plaintext: Bool = false
) {
self.consensusNetwork = consensusNetwork
self.mirrorNetwork = mirrorNetwork
self.eventLoop = eventLoop
self.plaintext = plaintext

if let updatePeriod {
updateTask = Self.makeTask(
Expand All @@ -78,7 +83,8 @@ internal actor NetworkUpdateTask {
startDelay: Self.networkFirstUpdateDelay,
updatePeriod: updatePeriod,
shard: shard,
realm: realm
realm: realm,
plaintext: plaintext
)
)
}
Expand All @@ -104,7 +110,8 @@ internal actor NetworkUpdateTask {
startDelay: nil,
updatePeriod: updatePeriod,
shard: shard,
realm: realm
realm: realm,
plaintext: plaintext
)
)
}
Expand Down Expand Up @@ -137,6 +144,9 @@ internal actor NetworkUpdateTask {

/// Realm number for address book file ID
internal let realm: UInt64

/// Whether to use only plaintext endpoints
internal let plaintext: Bool
}

// MARK: - Private Methods
Expand Down Expand Up @@ -168,15 +178,34 @@ internal actor NetworkUpdateTask {
FileId.getAddressBookFileIdFor(shard: config.shard, realm: config.realm)
).executeChannel(mirror.channel)

// Apply updates to consensus network atomically
let newNetwork = config.consensusNetwork.readCopyUpdate { old in
ConsensusNetwork.withAddressBook(old, eventLoop: config.eventLoop.next(), addressBook)
// Filter to plaintext-only endpoints if this is a plaintext-only client
let filtered: NodeAddressBook
if config.plaintext {
filtered = NodeAddressBook(
nodeAddresses: addressBook.nodeAddresses.map { address in
let plaintextEndpoints = address.serviceEndpoints.filter { endpoint in
endpoint.port == NodeConnection.consensusPlaintextPort
}

return NodeAddress(
nodeId: address.nodeId,
rsaPublicKey: address.rsaPublicKey,
nodeAccountId: address.nodeAccountId,
tlsCertificateHash: address.tlsCertificateHash,
serviceEndpoints: plaintextEndpoints,
description: address.description)
}
)
} else {
filtered = addressBook
}

_ = config.consensusNetwork.readCopyUpdate { network in
ConsensusNetwork.withAddressBook(network, eventLoop: config.eventLoop.next(), filtered)
}

// Log successful update with structured format
print(
"[Hiero.NetworkUpdate] Consensus network updated successfully"
)
print("[Hiero.NetworkUpdate] Consensus network updated successfully")

} catch let error as HError {
// Log error with structured format and context
Expand Down
Loading
Loading