diff --git a/Package.resolved b/Package.resolved index 8fb6fef4..721e1fb2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -9,6 +9,15 @@ "version" : "1.0.2" } }, + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client.git", + "state" : { + "revision" : "a22083713ee90808d527d0baa290c2fb13ca3096", + "version" : "1.21.1" + } + }, { "identity" : "cryptoswift", "kind" : "remoteSourceControl", @@ -36,6 +45,15 @@ "version" : "0.12.2" } }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms", + "state" : { + "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", + "version" : "1.2.0" + } + }, { "identity" : "swift-asn1", "kind" : "remoteSourceControl", @@ -104,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "3db5c4aeee8100d2db6f1eaf3864afdad5dc68fd", - "version" : "2.59.0" + "revision" : "359c461e5561d22c6334828806cc25d759ca7aa6", + "version" : "2.65.0" } }, { @@ -153,6 +171,15 @@ "version" : "2.4.2" } }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } + }, { "identity" : "swift-protobuf", "kind" : "remoteSourceControl", @@ -179,6 +206,15 @@ "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", "version" : "509.1.1" } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "f9266c85189c2751589a50ea5aec72799797e471", + "version" : "1.3.0" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 00a763f9..917c4172 100644 --- a/Package.swift +++ b/Package.swift @@ -97,6 +97,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.0.0"), + .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.21.1"), ], targets: [ .target( @@ -123,6 +124,7 @@ let package = Package( .product(name: "Atomics", package: "swift-atomics"), .product(name: "secp256k1", package: "secp256k1.swift"), "CryptoSwift", + .product(name: "AsyncHTTPClient", package: "async-http-client"), ] // todo: find some way to enable these locally. // swiftSettings: [ diff --git a/Sources/Hedera/Account/AccountBalance.swift b/Sources/Hedera/Account/AccountBalance.swift index 3e738d14..50f01213 100644 --- a/Sources/Hedera/Account/AccountBalance.swift +++ b/Sources/Hedera/Account/AccountBalance.swift @@ -21,7 +21,7 @@ import Foundation import HederaProtobufs -private struct TokenBalance { +internal struct TokenBalance { fileprivate let id: TokenId fileprivate let balance: UInt64 fileprivate let decimals: UInt32 @@ -59,6 +59,16 @@ public struct AccountBalance: Sendable { private let tokensInner: [TokenBalance] + internal init( + accountId: AccountId, + hbars: Hbar, + tokensInner: [TokenBalance] + ) { + self.accountId = accountId + self.hbars = hbars + self.tokensInner = tokensInner + } + // hack to work around deprecated warning private var tokenBalancesInner: [TokenId: UInt64] { Dictionary(uniqueKeysWithValues: tokensInner.map { ($0.id, $0.balance) }) diff --git a/Sources/Hedera/Account/AccountBalanceQuery.swift b/Sources/Hedera/Account/AccountBalanceQuery.swift index c2ba5953..3de350d0 100644 --- a/Sources/Hedera/Account/AccountBalanceQuery.swift +++ b/Sources/Hedera/Account/AccountBalanceQuery.swift @@ -85,11 +85,21 @@ public final class AccountBalanceQuery: Query { try await Proto_CryptoServiceAsyncClient(channel: channel).cryptoGetBalance(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { - guard case .cryptogetAccountBalance(let proto) = response else { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { + let mirrorNodeGateway = try MirrorNodeGateway.forNetwork(context.mirrorNetworkNodes, context.ledgerId) + let mirrorNodeService = MirrorNodeService.init(mirrorNodeGateway) + + guard case .cryptogetAccountBalance(var proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `cryptogetAccountBalance`") } + let tokenBalanceProto = try await mirrorNodeService.getTokenBalancesForAccount( + String(proto.accountID.accountNum)) + + proto.tokenBalances = tokenBalanceProto + return try .fromProtobuf(proto) } diff --git a/Sources/Hedera/Account/AccountInfo.swift b/Sources/Hedera/Account/AccountInfo.swift index 05165e18..c49a1f11 100644 --- a/Sources/Hedera/Account/AccountInfo.swift +++ b/Sources/Hedera/Account/AccountInfo.swift @@ -47,6 +47,7 @@ public struct AccountInfo: Sendable { maxAutomaticTokenAssociations: UInt32, aliasKey: PublicKey?, ethereumNonce: UInt64, + tokenRelationships: [TokenId: TokenRelationship], ledgerId: LedgerId, staking: StakingInfo? ) { @@ -66,6 +67,7 @@ public struct AccountInfo: Sendable { self.ethereumNonce = ethereumNonce self.ledgerId = ledgerId self.staking = staking + self.tokenRelationships = tokenRelationships self.guts = DeprecatedGuts( proxyAccountId: proxyAccountId, sendRecordThreshold: sendRecordThreshold, @@ -154,6 +156,9 @@ public struct AccountInfo: Sendable { /// Staking metadata for this account. public let staking: StakingInfo? + /// Token relationships for this account. + public let tokenRelationships: [TokenId: TokenRelationship] + /// Decode `Self` from protobuf-encoded `bytes`. /// /// - Throws: ``HError/ErrorKind/fromProtobuf`` if: @@ -178,6 +183,11 @@ extension AccountInfo: TryProtobufCodable { let staking = proto.hasStakingInfo ? proto.stakingInfo : nil let proxyAccountId = proto.hasProxyAccountID ? proto.proxyAccountID : nil + var tokenRelationships: [TokenId: TokenRelationship] = [:] + for relationship in proto.tokenRelationships { + tokenRelationships[.fromProtobuf(relationship.tokenID)] = try TokenRelationship.fromProtobuf(relationship) + } + self.init( accountId: try .fromProtobuf(proto.accountID), contractAccountId: proto.contractAccountID, @@ -196,6 +206,7 @@ extension AccountInfo: TryProtobufCodable { maxAutomaticTokenAssociations: UInt32(proto.maxAutomaticTokenAssociations), aliasKey: try .fromAliasBytes(proto.alias), ethereumNonce: UInt64(proto.ethereumNonce), + tokenRelationships: tokenRelationships, ledgerId: .fromBytes(proto.ledgerID), staking: try .fromProtobuf(staking) ) diff --git a/Sources/Hedera/Account/AccountInfoQuery.swift b/Sources/Hedera/Account/AccountInfoQuery.swift index b1b91b16..0670f84a 100644 --- a/Sources/Hedera/Account/AccountInfoQuery.swift +++ b/Sources/Hedera/Account/AccountInfoQuery.swift @@ -59,11 +59,21 @@ public final class AccountInfoQuery: Query { try await Proto_CryptoServiceAsyncClient(channel: channel).getAccountInfo(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { - guard case .cryptoGetInfo(let proto) = response else { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { + let mirrorNodeGateway = try MirrorNodeGateway.forNetwork(context.mirrorNetworkNodes, context.ledgerId) + let mirrorNodeService = MirrorNodeService.init(mirrorNodeGateway) + + guard case .cryptoGetInfo(var proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `cryptoGetInfo`") } + let tokenRelationshipsProto = try await mirrorNodeService.getTokenRelationshipsForAccount( + String(describing: try AccountId.fromProtobuf(proto.accountInfo.accountID).num)) + + proto.accountInfo.tokenRelationships = tokenRelationshipsProto + return try .fromProtobuf(proto.accountInfo) } diff --git a/Sources/Hedera/Account/AccountRecordsQuery.swift b/Sources/Hedera/Account/AccountRecordsQuery.swift index 39cc8e6e..220b3836 100644 --- a/Sources/Hedera/Account/AccountRecordsQuery.swift +++ b/Sources/Hedera/Account/AccountRecordsQuery.swift @@ -57,7 +57,9 @@ public final class AccountRecordsQuery: Query<[TransactionRecord]> { try await Proto_CryptoServiceAsyncClient(channel: channel).getAccountRecords(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { guard case .cryptoGetAccountRecords(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `cryptoGetAccountRecords`") } diff --git a/Sources/Hedera/Account/AccountStakersQuery.swift b/Sources/Hedera/Account/AccountStakersQuery.swift index 92ca7a45..cba1715b 100644 --- a/Sources/Hedera/Account/AccountStakersQuery.swift +++ b/Sources/Hedera/Account/AccountStakersQuery.swift @@ -57,7 +57,9 @@ public final class AccountStakersQuery: Query<[ProxyStaker]> { try await Proto_CryptoServiceAsyncClient(channel: channel).getStakersByAccountID(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { guard case .cryptoGetProxyStakers(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `cryptoGetProxyStakers`") } diff --git a/Sources/Hedera/ChunkedTransaction.swift b/Sources/Hedera/ChunkedTransaction.swift index bb3aa3e8..47dad4e8 100644 --- a/Sources/Hedera/ChunkedTransaction.swift +++ b/Sources/Hedera/ChunkedTransaction.swift @@ -206,7 +206,10 @@ extension ChunkedTransaction.FirstChunkView: Execute { self.transaction.regenerateTransactionId } - internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> ( + internal func makeRequest( + _ ledgerId: LedgerId?, _ mirrorNodeNetworks: [String], _ transactionId: TransactionId?, + _ nodeAccountId: AccountId + ) throws -> ( GrpcRequest, Context ) { assert(transaction.isFrozen) @@ -265,7 +268,10 @@ extension ChunkedTransaction.ChunkView: Execute { self.transaction.regenerateTransactionId } - internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> ( + internal func makeRequest( + _ ledgerId: LedgerId?, _ mirrorNodeNetworks: [String], _ transactionId: TransactionId?, + _ nodeAccountId: AccountId + ) throws -> ( GrpcRequest, Context ) { assert(transaction.isFrozen) diff --git a/Sources/Hedera/Contract/ContractBytecodeQuery.swift b/Sources/Hedera/Contract/ContractBytecodeQuery.swift index 7c4c4dc3..a5b11dbe 100644 --- a/Sources/Hedera/Contract/ContractBytecodeQuery.swift +++ b/Sources/Hedera/Contract/ContractBytecodeQuery.swift @@ -55,7 +55,9 @@ public final class ContractBytecodeQuery: Query { try await Proto_SmartContractServiceAsyncClient(channel: channel).contractGetBytecode(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { guard case .contractGetBytecodeResponse(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `contractGetBytecodeResponse`") } diff --git a/Sources/Hedera/Contract/ContractCallQuery.swift b/Sources/Hedera/Contract/ContractCallQuery.swift index 4d06a205..d0485f95 100644 --- a/Sources/Hedera/Contract/ContractCallQuery.swift +++ b/Sources/Hedera/Contract/ContractCallQuery.swift @@ -133,7 +133,9 @@ public final class ContractCallQuery: Query { try await Proto_SmartContractServiceAsyncClient(channel: channel).contractCallLocalMethod(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { guard case .contractCallLocal(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `contractCallLocal`") } diff --git a/Sources/Hedera/Contract/ContractInfo.swift b/Sources/Hedera/Contract/ContractInfo.swift index b0393883..a38ce300 100644 --- a/Sources/Hedera/Contract/ContractInfo.swift +++ b/Sources/Hedera/Contract/ContractInfo.swift @@ -61,6 +61,11 @@ public struct ContractInfo { /// The maximum number of tokens that a contract can be implicitly associated with. public let maxAutomaticTokenAssociations: UInt32 + /// The tokens associated to the contract + /// + /// Note: Query mirror node for token relationships. + public let tokenRelationships: [TokenId: TokenRelationship] + /// Ledger ID for the network the response was returned from. public let ledgerId: LedgerId @@ -91,6 +96,12 @@ extension ContractInfo: TryProtobufCodable { let autoRenewPeriod = proto.hasAutoRenewPeriod ? proto.autoRenewPeriod : nil let autoRenewAccountId = proto.hasAutoRenewAccountID ? proto.autoRenewAccountID : nil + var tokenRelationships: [TokenId: TokenRelationship] = [:] + + for relationship in proto.tokenRelationships { + tokenRelationships[.fromProtobuf(relationship.tokenID)] = try TokenRelationship.fromProtobuf(relationship) + } + self.init( contractId: try .fromProtobuf(proto.contractID), accountId: try .fromProtobuf(proto.accountID), @@ -104,6 +115,7 @@ extension ContractInfo: TryProtobufCodable { isDeleted: proto.deleted, autoRenewAccountId: try .fromProtobuf(autoRenewAccountId), maxAutomaticTokenAssociations: UInt32(proto.maxAutomaticTokenAssociations), + tokenRelationships: tokenRelationships, ledgerId: .fromBytes(proto.ledgerID), stakingInfo: try .fromProtobuf(proto.stakingInfo) ) diff --git a/Sources/Hedera/Contract/ContractInfoQuery.swift b/Sources/Hedera/Contract/ContractInfoQuery.swift index b0f02441..14e409de 100644 --- a/Sources/Hedera/Contract/ContractInfoQuery.swift +++ b/Sources/Hedera/Contract/ContractInfoQuery.swift @@ -54,17 +54,27 @@ public final class ContractInfoQuery: Query { try await Proto_SmartContractServiceAsyncClient(channel: channel).getContractInfo(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { - guard case .contractGetInfo(let proto) = response else { + internal override func makeQueryResponse(_ context: MirrorNetworkContext, _ response: Proto_Response.OneOf_Response) + async throws + -> Response + { + let mirrorNodeGateway = try MirrorNodeGateway.forNetwork(context.mirrorNetworkNodes, context.ledgerId) + let mirrorNodeService = MirrorNodeService.init(mirrorNodeGateway) + + guard case .contractGetInfo(var proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `contractGetInfo`") } - return try .fromProtobuf(proto.contractInfo) + let tokenRelationshipsProto = try await mirrorNodeService.getTokenRelationshipsForAccount( + String(describing: proto.contractInfo.accountID.accountNum)) + + proto.contractInfo.tokenRelationships = tokenRelationshipsProto + + return try ContractInfo.fromProtobuf(proto.contractInfo) } internal override func validateChecksums(on ledgerId: LedgerId) throws { try contractId?.validateChecksums(on: ledgerId) try super.validateChecksums(on: ledgerId) } - } diff --git a/Sources/Hedera/Execute.swift b/Sources/Hedera/Execute.swift index 7a7bddcd..ed2aebe5 100644 --- a/Sources/Hedera/Execute.swift +++ b/Sources/Hedera/Execute.swift @@ -60,7 +60,12 @@ internal protocol Execute { /// /// A created request is cached per node until any request returns /// `TransactionExpired`; in which case, the request cache is cleared. - func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> (GrpcRequest, Context) + func makeRequest( + _ ledgerId: LedgerId?, _ mirrorNodeNetworks: [String], _ transactionId: TransactionId?, + _ nodeAccountId: AccountId + ) throws -> ( + GrpcRequest, Context + ) func execute(_ channel: GRPCChannel, _ request: GrpcRequest) async throws -> GrpcResponse @@ -71,7 +76,7 @@ internal protocol Execute { _ context: Context, _ nodeAccountId: AccountId, _ transactionId: TransactionId? - ) throws -> Response + ) async throws -> Response func makeErrorPrecheck(_ status: Status, _ transactionId: TransactionId?) -> HError @@ -95,6 +100,8 @@ private struct ExecuteContext { fileprivate let network: Network fileprivate let backoffConfig: LegacyExponentialBackoff fileprivate let maxAttempts: Int + fileprivate let ledgerId: LedgerId? + fileprivate let mirrorNodeNetworks: [String] // timeout for a single grpc request. fileprivate let grpcTimeout: Duration? } @@ -150,6 +157,8 @@ internal func executeAny( network: client.net, backoffConfig: backoffBuilder, maxAttempts: backoff.maxAttempts, + ledgerId: client.ledgerId, + mirrorNodeNetworks: client.mirrorNetwork, grpcTimeout: nil ), executable: executable) @@ -185,7 +194,8 @@ private func executeAnyInner( } let (nodeAccountId, channel) = ctx.network.channel(for: nodeIndex) - let (request, context) = try executable.makeRequest(transactionId, nodeAccountId) + let (request, context) = try executable.makeRequest( + ctx.ledgerId, ctx.mirrorNodeNetworks, transactionId, nodeAccountId) let response: E.GrpcResponse do { @@ -222,7 +232,7 @@ private func executeAnyInner( break inner case .ok: - return try executable.makeResponse(response, context, nodeAccountId, transactionId) + return try await executable.makeResponse(response, context, nodeAccountId, transactionId) case .busy, .platformNotActive: // NOTE: this is a "busy" node @@ -309,6 +319,8 @@ private struct NodeIndexesGeneratorMap: AsyncSequence, AsyncIteratorProtocol { network: ctx.network, backoffConfig: ctx.backoffConfig, maxAttempts: ctx.maxAttempts, + ledgerId: ctx.ledgerId, + mirrorNodeNetworks: ctx.mirrorNodeNetworks, grpcTimeout: ctx.grpcTimeout ), executable: request diff --git a/Sources/Hedera/File/FileContentsQuery.swift b/Sources/Hedera/File/FileContentsQuery.swift index 035c3c2f..e132e90d 100644 --- a/Sources/Hedera/File/FileContentsQuery.swift +++ b/Sources/Hedera/File/FileContentsQuery.swift @@ -51,7 +51,9 @@ public final class FileContentsQuery: Query { try await Proto_FileServiceAsyncClient(channel: channel).getFileContent(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { guard case .fileGetContents(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `fileGetContents`") } diff --git a/Sources/Hedera/File/FileInfoQuery.swift b/Sources/Hedera/File/FileInfoQuery.swift index 03cd25b5..afadab47 100644 --- a/Sources/Hedera/File/FileInfoQuery.swift +++ b/Sources/Hedera/File/FileInfoQuery.swift @@ -54,7 +54,9 @@ public final class FileInfoQuery: Query { try await Proto_FileServiceAsyncClient(channel: channel).getFileInfo(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { guard case .fileGetInfo(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `fileGetInfo`") } diff --git a/Sources/Hedera/HError.swift b/Sources/Hedera/HError.swift index e66aba50..31ee1ca9 100644 --- a/Sources/Hedera/HError.swift +++ b/Sources/Hedera/HError.swift @@ -32,6 +32,7 @@ public struct HError: Error, CustomStringConvertible { case queryPaymentPreCheckStatus(status: Status, transactionId: TransactionId) case queryNoPaymentPreCheckStatus(status: Status) case basicParse + case mirrorNodeQuery case keyParse case keyDerive case noPayerAccountOrTransactionId @@ -86,6 +87,10 @@ public struct HError: Error, CustomStringConvertible { Self(kind: .basicParse, description: description) } + internal static func mirrorNodeQuery(_ description: String) -> Self { + Self(kind: .mirrorNodeQuery, description: description) + } + internal static func keyParse(_ description: String) -> Self { Self(kind: .keyParse, description: "failed to parse a key: \(description)") } diff --git a/Sources/Hedera/MirrorNodeGateway.swift b/Sources/Hedera/MirrorNodeGateway.swift new file mode 100644 index 00000000..cd41b661 --- /dev/null +++ b/Sources/Hedera/MirrorNodeGateway.swift @@ -0,0 +1,112 @@ +/* + * ‌ + * Hedera Swift SDK + * + * Copyright (C) 2022 - 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. + * + */ + +import AsyncHTTPClient +import Foundation +import GRPC +import HederaProtobufs +import NIO + +internal class MirrorNodeGateway { + internal var mirrorNodeUrl: String + private let httpClient: HTTPClient + + private init(mirrorNodeUrl: String) { + self.mirrorNodeUrl = mirrorNodeUrl + self.httpClient = HTTPClient(eventLoopGroupProvider: .singleton) + } + + deinit { + try? httpClient.syncShutdown() + } + + internal static func forClient(_ client: Client) throws -> MirrorNodeGateway { + let mirrorNodeUrl = try MirrorNodeRouter.getMirrorNodeUrl(client.mirrorNetwork, client.ledgerId) + + return .init(mirrorNodeUrl: mirrorNodeUrl) + } + + internal static func forNetwork(_ mirrorNetwork: [String], _ ledgerId: LedgerId?) throws -> MirrorNodeGateway { + let mirrorNodeUrl = try MirrorNodeRouter.getMirrorNodeUrl(mirrorNetwork, ledgerId) + + return .init(mirrorNodeUrl: mirrorNodeUrl) + } + + internal func getAccountInfo(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { + let fullApiUrl = MirrorNodeRouter.buildApiUrl( + self.mirrorNodeUrl, MirrorNodeRouter.MirrorNodeRoute.accountInfoRoute, idOrAliasOrEvmAddress) + + let responseBody = try await queryFromMirrorNode(fullApiUrl) + + let jsonObject = try await deserializeJson(responseBody) + + return jsonObject + } + + internal func getContractInfo(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { + let fullApiUrl = MirrorNodeRouter.buildApiUrl( + self.mirrorNodeUrl, MirrorNodeRouter.MirrorNodeRoute.contractInfoRoute, idOrAliasOrEvmAddress) + + let responseBody = try await queryFromMirrorNode(fullApiUrl) + + let jsonObject = try await deserializeJson(responseBody) + + return jsonObject + } + + internal func getAccountTokens(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { + let fullApiUrl = MirrorNodeRouter.buildApiUrl( + self.mirrorNodeUrl, MirrorNodeRouter.MirrorNodeRoute.tokenRelationshipsRoute, idOrAliasOrEvmAddress) + + let responseBody = try await queryFromMirrorNode(fullApiUrl) + + let jsonObject = try await deserializeJson(responseBody) + + return jsonObject + } + + private func queryFromMirrorNode(_ apiUrl: String) async throws -> String { + // Delay is needed to fetch data from the mirror node. + if apiUrl.contains("127.0.0.1:5551") { + try await Task.sleep(nanoseconds: 1_000_000_000 * 3) + } + + let request = HTTPClientRequest(url: apiUrl) + + let response: HTTPClientResponse = try await httpClient.execute(request, timeout: .seconds(30)) + + let body = try await response.body.collect(upTo: 1024 * 1024) + let bodyString = String(decoding: body.readableBytesView, as: UTF8.self) + + return bodyString + } + + func deserializeJson(_ responseBody: String) async throws -> [String: Any] { + guard let jsonData = responseBody.data(using: .utf8) else { + throw HError.mirrorNodeQuery("Response body is not valid UTF-8") + } + + guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { + throw HError.mirrorNodeQuery("Response body is not a valid JSON object") + } + + return jsonObject + } +} diff --git a/Sources/Hedera/MirrorNodeRouter.swift b/Sources/Hedera/MirrorNodeRouter.swift new file mode 100644 index 00000000..e55eb702 --- /dev/null +++ b/Sources/Hedera/MirrorNodeRouter.swift @@ -0,0 +1,62 @@ +/* + * ‌ + * Hedera Swift SDK + * + * Copyright (C) 2022 - 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. + * + */ + +import AsyncHTTPClient +import Atomics +import Foundation +import GRPC +import HederaProtobufs +import NIOCore + +internal struct MirrorNodeRouter { + static let apiVersion: String = "/api/v1" + + static let localNodePort = "5551" + + public enum MirrorNodeRoute: String { + case accountInfoRoute = "/accounts/%@" + case contractInfoRoute = "/contracts/%@" + case tokenRelationshipsRoute = "/accounts/%@/tokens" + } + + static func getMirrorNodeUrl(_ mirrorNetwork: [String], _ ledgerId: LedgerId?) throws -> String { + var mirrorNodeAddress: String = "" + + mirrorNetwork + .map { address in + address.prefix { $0 != ":" } + } + .first.map { mirrorNodeAddress = String($0) }! + + var fullMirrorNodeUrl: String + + if ledgerId != nil { + fullMirrorNodeUrl = String("https://\(mirrorNodeAddress)") + } else { + fullMirrorNodeUrl = String("http://\(mirrorNodeAddress):\(localNodePort)") + } + + return fullMirrorNodeUrl + } + + static func buildApiUrl(_ mirrorNodeUrl: String, _ route: MirrorNodeRoute, _ id: String) -> String { + return String("\(mirrorNodeUrl)\(apiVersion)\(route.rawValue.replacingOccurrences(of: "%@", with: id))") + } +} diff --git a/Sources/Hedera/MirrorNodeService.swift b/Sources/Hedera/MirrorNodeService.swift new file mode 100644 index 00000000..a7f37e07 --- /dev/null +++ b/Sources/Hedera/MirrorNodeService.swift @@ -0,0 +1,134 @@ +/* + * ‌ + * Hedera Swift SDK + * + * Copyright (C) 2022 - 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. + * + */ + +import AsyncHTTPClient +import Foundation +import HederaProtobufs + +internal final class MirrorNodeService { + internal var mirrorNodeGateway: MirrorNodeGateway + + init(_ mirrorNodeGateway: MirrorNodeGateway) { + self.mirrorNodeGateway = mirrorNodeGateway + } + + internal func getTokenBalancesForAccount(_ idNumOrEvmAddress: String) async throws -> [Proto_TokenBalance] { + let accountTokensResponse = try await self.mirrorNodeGateway.getAccountTokens(idNumOrEvmAddress) + + guard let tokens = accountTokensResponse["tokens"] else { + throw HError.mirrorNodeQuery("Error in fetching token relationships for account") + } + + guard let tokensList: [[String: Any]] = tokens as? [[String: Any]] else { + throw HError.mirrorNodeQuery("Error in converting tokens to array") + } + + let tokenBalances = try tokensList.map { token in + guard let id = token["token_id"] as? String, let tokenId = TokenId(id) else { + throw HError.mirrorNodeQuery("Error while converting `token id` to TokenId") + } + + guard let balance = token["balance"] as? UInt64 else { + throw HError.mirrorNodeQuery("Error while converting `balance` to unsigned int") + } + + guard let decimals = token["decimals"] as? UInt32 else { + throw HError.mirrorNodeQuery("Error while converting `decimals` to unsigned int") + } + + return Proto_TokenBalance.with { proto in + proto.tokenID = tokenId.toProtobuf() + proto.balance = balance + proto.decimals = decimals + } + } + + return tokenBalances + } + + internal func getTokenRelationshipsForAccount(_ idNumOrEvmAddress: String) async throws -> [Proto_TokenRelationship] + { + let accountTokensResponse = try await self.mirrorNodeGateway.getAccountTokens(idNumOrEvmAddress) + + guard let tokens = accountTokensResponse["tokens"] else { + throw HError.mirrorNodeQuery("Error in fetching token relationships for account") + } + + guard let tokensList: [[String: Any]] = tokens as? [[String: Any]] else { + throw HError.mirrorNodeQuery("Error in converting tokens to array") + } + + let tokenRelationships = try tokensList.map { token in + guard let id = token["token_id"] as? String, let tokenId = TokenId(id) else { + throw HError.mirrorNodeQuery("Error while converting `token id` to TokenId") + } + + guard let balance = token["balance"] as? UInt64 else { + throw HError.mirrorNodeQuery("Error while converting `balance` to unsigned int") + } + + guard let decimals = token["decimals"] as? UInt32 else { + throw HError.mirrorNodeQuery("Error while converting `decimals` to unsigned int") + } + + guard let kycStatus = token["kyc_status"] as? String else { + throw HError.mirrorNodeQuery("Error while converting `kyc status` to string") + } + + guard let freezeStatus = token["freeze_status"] as? String else { + throw HError.mirrorNodeQuery("Error while processing freeze status as string") + } + + guard let automaticAssociation = token["automatic_association"] as? Bool else { + throw HError.mirrorNodeQuery("Error while processing automatic association from token relationship") + } + + return try Proto_TokenRelationship.with { proto in + proto.tokenID = tokenId.toProtobuf() + proto.balance = balance + proto.decimals = decimals + proto.kycStatus = try getTokenKycStatusFromString(kycStatus) + proto.freezeStatus = try getTokenFreezeStatusFromString(freezeStatus) + proto.automaticAssociation = automaticAssociation + } + } + + return tokenRelationships + } + + internal func getTokenKycStatusFromString(_ tokenKycStatusString: String) throws -> Proto_TokenKycStatus { + switch tokenKycStatusString { + case "NOT_APPLICABLE": return Proto_TokenKycStatus.kycNotApplicable + case "GRANTED": return Proto_TokenKycStatus.granted + case "REVOKED": return Proto_TokenKycStatus.revoked + case _: throw HError.mirrorNodeQuery("Error while processing kyc status from token relationship") + } + } + + internal func getTokenFreezeStatusFromString(_ tokenFreezeStatusString: String) throws -> Proto_TokenFreezeStatus { + switch tokenFreezeStatusString { + case "NOT_APPLICABLE": return Proto_TokenFreezeStatus.freezeNotApplicable + case "FROZEN": return Proto_TokenFreezeStatus.frozen + case "UNFROZEN": return Proto_TokenFreezeStatus.unfrozen + case _: throw HError.mirrorNodeQuery("Error while processing freeze status from token relationship") + } + } + +} diff --git a/Sources/Hedera/NetworkVersionInfoQuery.swift b/Sources/Hedera/NetworkVersionInfoQuery.swift index d234bffc..5763f026 100644 --- a/Sources/Hedera/NetworkVersionInfoQuery.swift +++ b/Sources/Hedera/NetworkVersionInfoQuery.swift @@ -40,7 +40,9 @@ public final class NetworkVersionInfoQuery: Query { try await Proto_NetworkServiceAsyncClient(channel: channel).getVersionInfo(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { guard case .networkGetVersionInfo(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `networkGetVersionInfo`") } diff --git a/Sources/Hedera/PingQuery.swift b/Sources/Hedera/PingQuery.swift index b6071451..b2fef278 100644 --- a/Sources/Hedera/PingQuery.swift +++ b/Sources/Hedera/PingQuery.swift @@ -73,7 +73,12 @@ extension PingQuery: Execute { nil } - internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> (Proto_Query, ()) { + internal func makeRequest( + _ ledgerId: LedgerId?, _ mirrorNodeNetworks: [String], _ transactionId: TransactionId?, + _ nodeAccountId: AccountId + ) throws -> ( + Proto_Query, () + ) { let header = Proto_QueryHeader.with { $0.responseType = .answerOnly } assert(nodeAccountId == self.nodeAccountId) diff --git a/Sources/Hedera/Query.swift b/Sources/Hedera/Query.swift index dbed79e5..491ecf22 100644 --- a/Sources/Hedera/Query.swift +++ b/Sources/Hedera/Query.swift @@ -43,7 +43,9 @@ public class Query: ValidateChecksums { fatalError("Method `Query.queryExecute` must be overridden by `\(type(of: self))`") } - internal func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal func makeQueryResponse(_ context: MirrorNetworkContext, _ response: Proto_Response.OneOf_Response) + async throws -> Response + { fatalError("Method `Query.makeQueryResponse` must be overridden by `\(type(of: self))`") } @@ -186,12 +188,17 @@ public class Query: ValidateChecksums { } } +struct MirrorNetworkContext { + public let ledgerId: LedgerId? + public let mirrorNetworkNodes: [String] +} + extension Query: Execute { internal typealias GrpcRequest = Proto_Query internal typealias GrpcResponse = Proto_Response - internal typealias Context = () + internal typealias Context = MirrorNetworkContext internal var explicitTransactionId: TransactionId? { payment.transactionId @@ -217,17 +224,28 @@ extension Query: Execute { payment.index } - internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> (Proto_Query, ()) { + internal func makeRequest( + _ ledgerId: LedgerId?, _ mirrorNodeNetworks: [String], _ transactionId: TransactionId?, + _ nodeAccountId: AccountId + ) throws -> ( + Proto_Query, Context + ) { let request = toQueryProtobufWith( try .with { proto in proto.responseType = .answerOnly if requiresPayment { - proto.payment = try payment.makeRequest(transactionId, nodeAccountId).0 + proto.payment = try payment.makeRequest(ledgerId, mirrorNodeNetworks, transactionId, nodeAccountId) + .0 } }) - return (request, ()) + let context = MirrorNetworkContext( + ledgerId: ledgerId, + mirrorNetworkNodes: mirrorNodeNetworks + ) + + return (request, context) } internal func execute(_ channel: GRPC.GRPCChannel, _ request: Proto_Query) async throws -> Proto_Response { @@ -235,13 +253,14 @@ extension Query: Execute { } internal func makeResponse( - _ response: Proto_Response, _ context: (), _ nodeAccountId: AccountId, _ transactionId: TransactionId? - ) throws -> Response { + _ response: Proto_Response, _ context: MirrorNetworkContext, _ nodeAccountId: AccountId, + _ transactionId: TransactionId? + ) async throws -> Response { guard let response = response.response else { throw HError.fromProtobuf("unexpectly missing `response`") } - return try makeQueryResponse(response) + return try await makeQueryResponse(context, response) } internal func makeErrorPrecheck(_ status: Status, _ transactionId: TransactionId?) -> HError { diff --git a/Sources/Hedera/QueryCost.swift b/Sources/Hedera/QueryCost.swift index e60a36be..fd27b1e8 100644 --- a/Sources/Hedera/QueryCost.swift +++ b/Sources/Hedera/QueryCost.swift @@ -67,7 +67,12 @@ extension QueryCost: Execute { internal var requiresTransactionId: Bool { false } - internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> (Proto_Query, ()) { + internal func makeRequest( + _ ledgerId: LedgerId?, _ mirrorNodeNetworks: [String], _ transactionId: TransactionId?, + _ nodeAccountId: AccountId + ) throws -> ( + Proto_Query, () + ) { let request = query.toQueryProtobufWith( .with { proto in proto.responseType = .costAnswer diff --git a/Sources/Hedera/Schedule/ScheduleInfoQuery.swift b/Sources/Hedera/Schedule/ScheduleInfoQuery.swift index d1f121b6..787754b1 100644 --- a/Sources/Hedera/Schedule/ScheduleInfoQuery.swift +++ b/Sources/Hedera/Schedule/ScheduleInfoQuery.swift @@ -54,7 +54,9 @@ public final class ScheduleInfoQuery: Query { try await Proto_ScheduleServiceAsyncClient(channel: channel).getScheduleInfo(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { guard case .scheduleGetInfo(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `scheduleGetInfo`") } diff --git a/Sources/Hedera/Token/TokenInfoQuery.swift b/Sources/Hedera/Token/TokenInfoQuery.swift index c43bfc8b..9e30fcc7 100644 --- a/Sources/Hedera/Token/TokenInfoQuery.swift +++ b/Sources/Hedera/Token/TokenInfoQuery.swift @@ -54,7 +54,9 @@ public final class TokenInfoQuery: Query { try await Proto_TokenServiceAsyncClient(channel: channel).getTokenInfo(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { guard case .tokenGetInfo(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `tokenGetInfo`") } diff --git a/Sources/Hedera/Token/TokenNftInfoQuery.swift b/Sources/Hedera/Token/TokenNftInfoQuery.swift index 4f218668..3c96982f 100644 --- a/Sources/Hedera/Token/TokenNftInfoQuery.swift +++ b/Sources/Hedera/Token/TokenNftInfoQuery.swift @@ -54,7 +54,9 @@ public final class TokenNftInfoQuery: Query { try await Proto_TokenServiceAsyncClient(channel: channel).getTokenNftInfo(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { guard case .tokenGetNftInfo(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `tokenGetNftInfo`") } diff --git a/Sources/Hedera/Token/TokenRelationship.swift b/Sources/Hedera/Token/TokenRelationship.swift new file mode 100644 index 00000000..61bdcfe6 --- /dev/null +++ b/Sources/Hedera/Token/TokenRelationship.swift @@ -0,0 +1,141 @@ +/* + * ‌ + * Hedera Swift SDK + * + * Copyright (C) 2022 - 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. + * + */ + +import GRPC +import HederaProtobufs + +/// Token's information related to the given Account. +/// +/// See Hedera +/// Documentation +public struct TokenRelationship: Sendable { + /// The ID of the token + public let tokenId: TokenId + + /// The Symbol of the token + public let symbol: String + + /// For token of type FUNGIBLE_COMMON - the balance that the Account holds in the smallest + /// denomination. + /// + /// For token of type NON_FUNGIBLE_UNIQUE - the number of NFTs held by the account + public let balance: UInt64 + + /// The KYC status of the account (KycNotApplicable, Granted or Revoked). + /// + /// If the token does not have KYC key, KycNotApplicable is returned + public let kycStatus: UInt32 + + /// The Freeze status of the account (FreezeNotApplicable, Frozen or Unfrozen). + /// + /// If the token does not have Freeze key, FreezeNotApplicable is returned + public let freezeStatus: UInt32 + + /// Specifies if the relationship is created implicitly. + /// + /// False : explicitly associated, + /// True : implicitly associated. + public let automaticAssociation: Bool + + public init( + tokenId: TokenId, symbol: String, balance: UInt64, kycStatus: UInt32, freezeStatus: UInt32, + automaticAssociation: Bool + ) { + self.tokenId = tokenId + self.symbol = symbol + self.balance = balance + self.kycStatus = kycStatus + self.freezeStatus = freezeStatus + self.automaticAssociation = automaticAssociation + } + +} + +extension TokenRelationship: TryProtobufCodable { + internal typealias Protobuf = Proto_TokenRelationship + + internal init(protobuf proto: Protobuf) throws { + var freezeStatus: UInt32 + var kycStatus: UInt32 + + switch proto.freezeStatus { + case .freezeNotApplicable: + freezeStatus = 0 + case .frozen: + freezeStatus = 1 + case .unfrozen: + freezeStatus = 2 + case .unrecognized(_): + throw HError.fromProtobuf("invalid freeze status from protobuf: \(proto.freezeStatus)") + } + + switch proto.kycStatus { + case .kycNotApplicable: + kycStatus = 0 + case .granted: + kycStatus = 1 + case .revoked: + kycStatus = 2 + case .unrecognized(_): + throw HError.fromProtobuf("invalid kyc status from protobuf: \(proto.kycStatus)") + } + + self.init( + tokenId: .fromProtobuf(proto.tokenID), symbol: proto.symbol, balance: proto.balance, kycStatus: kycStatus, + freezeStatus: freezeStatus, automaticAssociation: proto.automaticAssociation) + + } + + internal func toProtobuf() -> Protobuf { + .with { proto in + var protoFreezeStatus: Proto_TokenFreezeStatus + var protoKycStatus: Proto_TokenKycStatus + + switch freezeStatus { + case 0: + protoFreezeStatus = .freezeNotApplicable + case 1: + protoFreezeStatus = .frozen + case 2: + protoFreezeStatus = .unfrozen + default: + fatalError("Unrecognized Freeze Status") + } + + switch kycStatus { + case 0: + protoKycStatus = .kycNotApplicable + case 1: + protoKycStatus = .granted + case 2: + protoKycStatus = .revoked + default: + fatalError("Unrecognized KYC Status") + } + + proto.tokenID = tokenId.toProtobuf() + proto.balance = balance + proto.symbol = symbol + proto.freezeStatus = protoFreezeStatus + proto.kycStatus = protoKycStatus + proto.automaticAssociation = automaticAssociation + } + } +} diff --git a/Sources/Hedera/Topic/TopicInfoQuery.swift b/Sources/Hedera/Topic/TopicInfoQuery.swift index 2d1320e7..a8f7640e 100644 --- a/Sources/Hedera/Topic/TopicInfoQuery.swift +++ b/Sources/Hedera/Topic/TopicInfoQuery.swift @@ -54,7 +54,9 @@ public final class TopicInfoQuery: Query { try await Proto_ConsensusServiceAsyncClient(channel: channel).getTopicInfo(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { guard case .consensusGetTopicInfo(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `consensusGetTopicInfo`") } diff --git a/Sources/Hedera/Transaction.swift b/Sources/Hedera/Transaction.swift index d5cb5102..99ae9c5e 100644 --- a/Sources/Hedera/Transaction.swift +++ b/Sources/Hedera/Transaction.swift @@ -593,7 +593,10 @@ extension Transaction: Execute { self.operator?.accountId } - internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> ( + internal func makeRequest( + _ ledgerId: LedgerId?, _ mirrorNodeNetworks: [String], _ transactionId: TransactionId?, + _ nodeAccountId: AccountId + ) throws -> ( GrpcRequest, TransactionHash ) { assert(isFrozen) diff --git a/Sources/Hedera/Transaction/TransactionSources.swift b/Sources/Hedera/Transaction/TransactionSources.swift index ad864028..b9460e6f 100644 --- a/Sources/Hedera/Transaction/TransactionSources.swift +++ b/Sources/Hedera/Transaction/TransactionSources.swift @@ -428,7 +428,10 @@ extension SourceTransactionExecuteView: Execute { nil } - internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> ( + internal func makeRequest( + _ ledgerId: LedgerId?, _ mirrorNodeNetworks: [String], _ transactionId: TransactionId?, + _ nodeAccountId: AccountId + ) throws -> ( GrpcRequest, Context ) { assert(transactionId == chunk.transactionId) diff --git a/Sources/Hedera/TransactionReceiptQuery.swift b/Sources/Hedera/TransactionReceiptQuery.swift index bb5c1d47..1b41830c 100644 --- a/Sources/Hedera/TransactionReceiptQuery.swift +++ b/Sources/Hedera/TransactionReceiptQuery.swift @@ -94,7 +94,9 @@ public final class TransactionReceiptQuery: Query { internal override var relatedTransactionId: TransactionId? { transactionId } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { guard case .transactionGetReceipt(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `transactionGetReceipt`") } diff --git a/Sources/Hedera/TransactionRecordQuery.swift b/Sources/Hedera/TransactionRecordQuery.swift index d9765f92..78bad1a6 100644 --- a/Sources/Hedera/TransactionRecordQuery.swift +++ b/Sources/Hedera/TransactionRecordQuery.swift @@ -87,7 +87,9 @@ public final class TransactionRecordQuery: Query { try await Proto_CryptoServiceAsyncClient(channel: channel).getTxRecordByTxID(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws + -> Response + { guard case .transactionGetRecord(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `transactionGetRecord`") } diff --git a/Tests/HederaE2ETests/Account/AccountBalance.swift b/Tests/HederaE2ETests/Account/AccountBalance.swift index 14906cad..fb230e79 100644 --- a/Tests/HederaE2ETests/Account/AccountBalance.swift +++ b/Tests/HederaE2ETests/Account/AccountBalance.swift @@ -22,7 +22,7 @@ import Hedera import XCTest internal final class AccountBalance: XCTestCase { - internal func testQuery() async throws { + internal func testQueryBoo() async throws { let testEnv = TestEnvironment.global guard let op = testEnv.operator else { @@ -114,48 +114,46 @@ internal final class AccountBalance: XCTestCase { } } - // disabled because swift doesn't have a way to ignore deprecated warnings. - // internal func testQueryTokenBalances() async throws { - // let testEnv = try TestEnvironment.nonFree - - // let account = try await Account.create(testEnv, balance: 10) - - // addTeardownBlock { try await account.delete(testEnv) } - - // let receipt = try await TokenCreateTransaction() - // .name("ffff") - // .symbol("f") - // .initialSupply(10000) - // .decimals(50) - // .treasuryAccountId(account.id) - // .expirationTime(.now + .minutes(5)) - // .adminKey(.single(account.key.publicKey)) - // .supplyKey(.single(account.key.publicKey)) - // .freezeDefault(false) - // .sign(account.key) - // .execute(testEnv.client) - // .getReceipt(testEnv.client) - - // let tokenId = try XCTUnwrap(receipt.tokenId) - - // addTeardownBlock { - // _ = try await TokenBurnTransaction() - // .tokenId(tokenId) - // .amount(10000) - // .sign(account.key) - // .execute(testEnv.client) - // .getReceipt(testEnv.client) - - // _ = try await TokenDeleteTransaction() - // .tokenId(tokenId) - // .sign(account.key) - // .execute(testEnv.client) - // .getReceipt(testEnv.client) - // } - - // let _ = try await AccountBalanceQuery().accountId(account.id).execute(testEnv.client) - - // // XCTAssertEqual(balance.tokenBalances[tokenId], 10000) - // // XCTAssertEqual(balance.tokenDecimals[tokenId], 50) - // } + internal func testQueryTokenBalances() async throws { + let testEnv = try TestEnvironment.nonFree + + let account = try await Account.create(testEnv, balance: 10) + + addTeardownBlock { try await account.delete(testEnv) } + + let receipt = try await TokenCreateTransaction() + .name("ffff") + .symbol("f") + .initialSupply(10000) + .decimals(50) + .treasuryAccountId(account.id) + .expirationTime(.now + .minutes(5)) + .adminKey(.single(account.key.publicKey)) + .supplyKey(.single(account.key.publicKey)) + .freezeDefault(false) + .sign(account.key) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + let tokenId = try XCTUnwrap(receipt.tokenId) + + addTeardownBlock { + _ = try await TokenBurnTransaction() + .tokenId(tokenId) + .amount(10000) + .sign(account.key) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + _ = try await TokenDeleteTransaction() + .tokenId(tokenId) + .sign(account.key) + .execute(testEnv.client) + .getReceipt(testEnv.client) + } + + let balance = try await AccountBalanceQuery().accountId(account.id).execute(testEnv.client) + + XCTAssertEqual(balance.hbars, 10) + } } diff --git a/Tests/HederaE2ETests/Contract/ContractCreate.swift b/Tests/HederaE2ETests/Contract/ContractCreate.swift index 1f32ce5b..3efd1ccf 100644 --- a/Tests/HederaE2ETests/Contract/ContractCreate.swift +++ b/Tests/HederaE2ETests/Contract/ContractCreate.swift @@ -92,12 +92,11 @@ internal final class ContractCreate: XCTestCase { .constructorParameters(ContractFunctionParameters().addString("Hello from Hedera.")) .bytecodeFileId(bytecode.fileId) .contractMemo("[e2e::ContractCreateTransaction]") - .execute(testEnv.client) - .getReceipt(testEnv.client), + .execute(testEnv.client), "expected error creating contract" ) { error in - guard case .receiptStatus(let status, transactionId: _) = error.kind else { - XCTFail("`\(error.kind)` is not `.receiptStatus`") + guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { + XCTFail("`\(error.kind)` is not `.transactionPreCheckStatus`") return } diff --git a/Tests/HederaTests/__Snapshots__/ContractInfoTests/testFromBytes.1.txt b/Tests/HederaTests/__Snapshots__/ContractInfoTests/testFromBytes.1.txt index 3bd5aa11..0cf20454 100644 --- a/Tests/HederaTests/__Snapshots__/ContractInfoTests/testFromBytes.1.txt +++ b/Tests/HederaTests/__Snapshots__/ContractInfoTests/testFromBytes.1.txt @@ -1 +1 @@ -ContractInfo(contractId: 0.0.1, accountId: 0.0.5006, contractAccountId: "3", adminKey: nil, expirationTime: Optional(1554158728000000000), autoRenewPeriod: Optional(Hedera.Duration(seconds: 432000)), storage: 0, contractMemo: "flook", balance: 8 tℏ, isDeleted: false, autoRenewAccountId: nil, maxAutomaticTokenAssociations: 0, ledgerId: testnet, stakingInfo: Hedera.StakingInfo(declineStakingReward: false, stakePeriodStart: nil, pendingReward: 0 tℏ, stakedToMe: 0 tℏ, stakedAccountId: nil, stakedNodeId: nil)) \ No newline at end of file +ContractInfo(contractId: 0.0.1, accountId: 0.0.5006, contractAccountId: "3", adminKey: nil, expirationTime: Optional(1554158728000000000), autoRenewPeriod: Optional(Hedera.Duration(seconds: 432000)), storage: 0, contractMemo: "flook", balance: 8 tℏ, isDeleted: false, autoRenewAccountId: nil, maxAutomaticTokenAssociations: 0, tokenRelationships: [:], ledgerId: testnet, stakingInfo: Hedera.StakingInfo(declineStakingReward: false, stakePeriodStart: nil, pendingReward: 0 tℏ, stakedToMe: 0 tℏ, stakedAccountId: nil, stakedNodeId: nil)) \ No newline at end of file diff --git a/Tests/HederaTests/__Snapshots__/ContractInfoTests/testFromProtobuf.1.txt b/Tests/HederaTests/__Snapshots__/ContractInfoTests/testFromProtobuf.1.txt index 3bd5aa11..0cf20454 100644 --- a/Tests/HederaTests/__Snapshots__/ContractInfoTests/testFromProtobuf.1.txt +++ b/Tests/HederaTests/__Snapshots__/ContractInfoTests/testFromProtobuf.1.txt @@ -1 +1 @@ -ContractInfo(contractId: 0.0.1, accountId: 0.0.5006, contractAccountId: "3", adminKey: nil, expirationTime: Optional(1554158728000000000), autoRenewPeriod: Optional(Hedera.Duration(seconds: 432000)), storage: 0, contractMemo: "flook", balance: 8 tℏ, isDeleted: false, autoRenewAccountId: nil, maxAutomaticTokenAssociations: 0, ledgerId: testnet, stakingInfo: Hedera.StakingInfo(declineStakingReward: false, stakePeriodStart: nil, pendingReward: 0 tℏ, stakedToMe: 0 tℏ, stakedAccountId: nil, stakedNodeId: nil)) \ No newline at end of file +ContractInfo(contractId: 0.0.1, accountId: 0.0.5006, contractAccountId: "3", adminKey: nil, expirationTime: Optional(1554158728000000000), autoRenewPeriod: Optional(Hedera.Duration(seconds: 432000)), storage: 0, contractMemo: "flook", balance: 8 tℏ, isDeleted: false, autoRenewAccountId: nil, maxAutomaticTokenAssociations: 0, tokenRelationships: [:], ledgerId: testnet, stakingInfo: Hedera.StakingInfo(declineStakingReward: false, stakePeriodStart: nil, pendingReward: 0 tℏ, stakedToMe: 0 tℏ, stakedAccountId: nil, stakedNodeId: nil)) \ No newline at end of file diff --git a/protobufs b/protobufs index e650477f..6096d432 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit e650477fc5f061fb9229db67944a7911fa5c65bc +Subproject commit 6096d432efcb9bc76bb0b813cf17c1b001e36335