From dfab82e352a227479eaddd0bdb8befb4abea4cae Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Tue, 14 May 2024 17:35:10 -0700 Subject: [PATCH 01/18] chore: update protobufs Signed-off-by: Ricky Saechao --- protobufs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protobufs b/protobufs index e650477f..17fb1486 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit e650477fc5f061fb9229db67944a7911fa5c65bc +Subproject commit 17fb148652b1e2badda1c5cdff531f0fcf7f4c55 From 89dc2d9ea51e88f496f662b4e125daa1db708a46 Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Tue, 28 May 2024 15:58:47 -0700 Subject: [PATCH 02/18] feat: add mirror node service, gateway, and router, Signed-off-by: Ricky Saechao --- Sources/Hedera/MirrorNodeGateway.swift | 8 ++++++++ Sources/Hedera/MirrorNodeRouter.swift | 8 ++++++++ Sources/Hedera/MirrorNodeService.swift | 8 ++++++++ 3 files changed, 24 insertions(+) create mode 100644 Sources/Hedera/MirrorNodeGateway.swift create mode 100644 Sources/Hedera/MirrorNodeRouter.swift create mode 100644 Sources/Hedera/MirrorNodeService.swift diff --git a/Sources/Hedera/MirrorNodeGateway.swift b/Sources/Hedera/MirrorNodeGateway.swift new file mode 100644 index 00000000..2fe1941a --- /dev/null +++ b/Sources/Hedera/MirrorNodeGateway.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Ricky Saechao on 5/27/24. +// + +import Foundation diff --git a/Sources/Hedera/MirrorNodeRouter.swift b/Sources/Hedera/MirrorNodeRouter.swift new file mode 100644 index 00000000..2fe1941a --- /dev/null +++ b/Sources/Hedera/MirrorNodeRouter.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Ricky Saechao on 5/27/24. +// + +import Foundation diff --git a/Sources/Hedera/MirrorNodeService.swift b/Sources/Hedera/MirrorNodeService.swift new file mode 100644 index 00000000..2fe1941a --- /dev/null +++ b/Sources/Hedera/MirrorNodeService.swift @@ -0,0 +1,8 @@ +// +// File.swift +// +// +// Created by Ricky Saechao on 5/27/24. +// + +import Foundation From 042bb213099116f358e344c52f6f5c2af1385740 Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Tue, 28 May 2024 15:59:53 -0700 Subject: [PATCH 03/18] feat: add mirror node service, gateway, and router, Signed-off-by: Ricky Saechao --- Package.resolved | 40 ++++++- Package.swift | 2 + Sources/Hedera/MirrorNodeGateway.swift | 117 ++++++++++++++++++- Sources/Hedera/MirrorNodeRouter.swift | 76 +++++++++++- Sources/Hedera/MirrorNodeService.swift | 156 ++++++++++++++++++++++++- 5 files changed, 371 insertions(+), 20 deletions(-) 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..1bb122fb 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/MirrorNodeGateway.swift b/Sources/Hedera/MirrorNodeGateway.swift index 2fe1941a..6ded22f1 100644 --- a/Sources/Hedera/MirrorNodeGateway.swift +++ b/Sources/Hedera/MirrorNodeGateway.swift @@ -1,8 +1,113 @@ -// -// File.swift -// -// -// Created by Ricky Saechao on 5/27/24. -// +/* + * ‌ + * 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 Atomics +import GRPC +import NIO import Foundation +import HederaProtobufs +import AsyncHTTPClient + +internal final class MirrorNodeGateway { + internal var mirrorNodeUrl: String + + private init(mirrorNodeUrl: String) { + self.mirrorNodeUrl = mirrorNodeUrl + } + + 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] { + var fullApiUrl = MirrorNodeRouter.buildApiUrl(self.mirrorNodeUrl, MirrorNodeRouter.ACCOUNTS_ROUTE, idOrAliasOrEvmAddress) + + let responseBody = try await queryFromMirrorNode(fullApiUrl) + + guard let jsonData = responseBody.data(using: .utf8) else { + throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) + } + + guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { + throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Response body is not a valid JSON object"]) + } + + return jsonObject + } + + internal func getContractInfo(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { + var fullApiUrl = MirrorNodeRouter.buildApiUrl(self.mirrorNodeUrl, MirrorNodeRouter.CONTRACTS_ROUTE, idOrAliasOrEvmAddress) + + let responseBody = try await queryFromMirrorNode(fullApiUrl) + + guard let jsonData = responseBody.data(using: .utf8) else { + throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) + } + + guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { + throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Response body is not a valid JSON object"]) + } + + return jsonObject + } + + internal func getAccountTokens(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { + var fullApiUrl = MirrorNodeRouter.buildApiUrl(self.mirrorNodeUrl, MirrorNodeRouter.ACCOUNTS_ROUTE, idOrAliasOrEvmAddress) + + let responseBody = try await queryFromMirrorNode(fullApiUrl) + + guard let jsonData = responseBody.data(using: .utf8) else { + throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) + } + + guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { + throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Response body is not a valid JSON object"]) + } + + return jsonObject + } + + internal func queryFromMirrorNode(_ apiUrl: String) async throws -> String { + let httpClient = HTTPClient(eventLoopGroupProvider: .singleton) + defer { + try? httpClient.syncShutdown() + } + + var request = HTTPClientRequest(url: apiUrl) + request.method = .GET + + let response = 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 + } +} + + diff --git a/Sources/Hedera/MirrorNodeRouter.swift b/Sources/Hedera/MirrorNodeRouter.swift index 2fe1941a..28837049 100644 --- a/Sources/Hedera/MirrorNodeRouter.swift +++ b/Sources/Hedera/MirrorNodeRouter.swift @@ -1,8 +1,72 @@ -// -// File.swift -// -// -// Created by Ricky Saechao on 5/27/24. -// +/* + * ‌ + * 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 Atomics +import GRPC +import NIOCore import Foundation +import HederaProtobufs +import AsyncHTTPClient + +internal final class MirrorNodeRouter { + static let API_VERSION: String = "/api/v1" + + static let LOCAL_NODE_PORT = "5551" + + public static let ACCOUNTS_ROUTE = "accounts" + public static let CONTRACTS_ROUTE = "contracts" + public static let ACCOUNT_TOKENS_ROUTE = "accounts_tokens" + + static let routes: [String: String] = [ + ACCOUNTS_ROUTE: "/accounts/%@", + CONTRACTS_ROUTE: "/contracts/%@", + ACCOUNT_TOKENS_ROUTE: "/accounts/%@/tokens", + ] + + private func MirrorNodeRouter() {} + + static func getMirrorNodeUrl(_ mirrorNetwork: [String], _ ledgerId: LedgerId?) throws -> String { + let mirrorNodeAddress: String? = mirrorNetwork + .map { address in + address.prefix { $0 != ":" } + } + .first.map { String($0) } + + if mirrorNodeAddress == nil { + fatalError("Mirror address not found") + } + + var fullMirrorNodeUrl: String + + if ledgerId != nil { + fullMirrorNodeUrl = String("http://\(mirrorNodeAddress)") + } else { + fullMirrorNodeUrl = String("http://\(mirrorNodeAddress):\(LOCAL_NODE_PORT)") + } + + return fullMirrorNodeUrl + } + + static func buildApiUrl(_ mirrorNodeUrl: String, _ route: String, _ id: String) -> String { + return String("\(mirrorNodeUrl)\(API_VERSION)\(String(format: "\(String(describing: routes[route]))", id))") + } +} + + diff --git a/Sources/Hedera/MirrorNodeService.swift b/Sources/Hedera/MirrorNodeService.swift index 2fe1941a..189b2433 100644 --- a/Sources/Hedera/MirrorNodeService.swift +++ b/Sources/Hedera/MirrorNodeService.swift @@ -1,8 +1,152 @@ -// -// File.swift -// -// -// Created by Ricky Saechao on 5/27/24. -// +/* + * ‌ + * 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 Foundation +import HederaProtobufs +import AsyncHTTPClient + +internal final class MirrorNodeService { + internal var mirrorNodeGateway: MirrorNodeGateway + + private init(mirrorNodeGateway: MirrorNodeGateway) { + self.mirrorNodeGateway = mirrorNodeGateway + } + + internal func getAccountNum(_ evmAddress: String) async throws -> UInt64 { + let accountInfoResponse = try await self.mirrorNodeGateway.getAccountInfo(evmAddress) + + guard let accountId = accountInfoResponse["account"] else { + throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error while processing getAccountInfo mirror node query"]) + } + + let accountNum = AccountId(String(describing: accountId))?.num + + return accountNum! + } + + internal func getAccountEvmAddress(_ num: UInt64) async throws -> EvmAddress { + let accountInfoResponse = try await self.mirrorNodeGateway.getAccountInfo(String(describing: num)) + + guard let addressAny = accountInfoResponse["evm_address"] else { + throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error while processing getAccountEvmAddress mirror node query"]) + } + + let evmAddress = AccountId(String(describing: addressAny))?.evmAddress + + return evmAddress! + } + + internal func getContractNum(_ evmAddress: String) async throws -> UInt64 { + let accountInfoResponse = try await self.mirrorNodeGateway.getContractInfo(evmAddress) + + guard let contractId = accountInfoResponse["contract_id"] else { + throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error while processing getAccountInfo mirror node query"]) + } + + let contractIdNum = ContractId(String(describing: contractId))?.num + + return contractIdNum! + } + + internal func getTokenBalancesForAccount(_ evmAddress: String) async throws -> [Proto_TokenBalance] { + let accountTokensResponse = try await self.mirrorNodeGateway.getAccountTokens(evmAddress) + + guard let tokens = accountTokensResponse["tokens"] else { + throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error while processing getTokenBalancesForAccount mirror node query"]) + } + + var tokenBalances: [Proto_TokenBalance] = [] + + if let tokensList = tokens as? [[String: Any]] { + tokensList.forEach {token in + let tokenId = TokenId(String(describing: token["token_id"]))?.toProtobuf() + let balance = UInt64(String(describing: token["balance"])) + let decimals = UInt32(String(describing: token["decimals"])) + + let tokenBalanceProto = Proto_TokenBalance.with { proto in + proto.tokenID = tokenId! + proto.balance = balance! + proto.decimals = decimals! + } + + tokenBalances.append(tokenBalanceProto) + } + } else { + throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error while processing getTokenBalancesForAccount mirror node query"]) + } + + return tokenBalances + } + + internal func getTokenRelationshipsForAccount(_ evmAddress: String) async throws -> [Proto_TokenRelationship] { + let accountTokensResponse = try await self.mirrorNodeGateway.getAccountTokens(evmAddress) + + guard let tokens = accountTokensResponse["tokens"] else { + throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error while processing getTokenRelationshipsForAccount mirror node query"]) + } + + var tokenBalances: [Proto_TokenRelationship] = [] + + if let tokensList = tokens as? [[String: Any]] { + try tokensList.forEach {token in + let tokenId = TokenId(String(describing: token["token_id"]))?.toProtobuf() + let balance = UInt64(String(describing: token["balance"])) + let decimals = UInt32(String(describing: token["decimals"])) + let kycStatus = String(describing: token["kyc_status"]) + let freezeStatus = String(describing: token["freeze_status"]) + let automaticAssociation = Bool(String(describing: token["automatic_assocation"])) + + let tokenRelationshipsProto = try Proto_TokenRelationship.with { proto in + proto.tokenID = tokenId! + proto.balance = balance! + proto.decimals = decimals! + proto.kycStatus = try getTokenKycStatusFromString(kycStatus) + proto.freezeStatus = try getTokenFreezeStatusFromString(freezeStatus) + proto.automaticAssociation = automaticAssociation! + } + + tokenBalances.append(tokenRelationshipsProto) + } + } else { + throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error while processing getTokenRelationshipsForAccount mirror node query"]) + } + + return tokenBalances + } + + 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 NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid token KYC status: \(tokenKycStatusString)"]) + } + } + + 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 NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid token freeze status: \(tokenFreezeStatusString)"]) + } + } + +} From 22342ba154b940bbdfdb0ad901d0e9e7bf720de3 Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Tue, 28 May 2024 20:33:11 -0700 Subject: [PATCH 04/18] chore: format Signed-off-by: Ricky Saechao --- Package.swift | 2 +- Sources/Hedera/MirrorNodeGateway.swift | 88 ++++++++++--------- Sources/Hedera/MirrorNodeRouter.swift | 31 ++++--- Sources/Hedera/MirrorNodeService.swift | 112 +++++++++++++------------ 4 files changed, 125 insertions(+), 108 deletions(-) diff --git a/Package.swift b/Package.swift index 1bb122fb..917c4172 100644 --- a/Package.swift +++ b/Package.swift @@ -124,7 +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") + .product(name: "AsyncHTTPClient", package: "async-http-client"), ] // todo: find some way to enable these locally. // swiftSettings: [ diff --git a/Sources/Hedera/MirrorNodeGateway.swift b/Sources/Hedera/MirrorNodeGateway.swift index 6ded22f1..503eb452 100644 --- a/Sources/Hedera/MirrorNodeGateway.swift +++ b/Sources/Hedera/MirrorNodeGateway.swift @@ -18,80 +18,94 @@ * */ -import Atomics -import GRPC -import NIO +import AsyncHTTPClient import Foundation +import GRPC import HederaProtobufs -import AsyncHTTPClient +import NIO -internal final class MirrorNodeGateway { +internal struct MirrorNodeGateway { internal var mirrorNodeUrl: String private init(mirrorNodeUrl: String) { self.mirrorNodeUrl = mirrorNodeUrl } - + 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] { - var fullApiUrl = MirrorNodeRouter.buildApiUrl(self.mirrorNodeUrl, MirrorNodeRouter.ACCOUNTS_ROUTE, idOrAliasOrEvmAddress) - + var fullApiUrl = MirrorNodeRouter.buildApiUrl( + self.mirrorNodeUrl, MirrorNodeRouter.ACCOUNTS_ROUTE, idOrAliasOrEvmAddress) + let responseBody = try await queryFromMirrorNode(fullApiUrl) - + guard let jsonData = responseBody.data(using: .utf8) else { - throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) + throw NSError( + domain: "InvalidResponseError", code: -1, + userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) } - + guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { - throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Response body is not a valid JSON object"]) + throw NSError( + domain: "InvalidResponseError", code: -1, + userInfo: [NSLocalizedDescriptionKey: "Response body is not a valid JSON object"]) } - + return jsonObject } - + internal func getContractInfo(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { - var fullApiUrl = MirrorNodeRouter.buildApiUrl(self.mirrorNodeUrl, MirrorNodeRouter.CONTRACTS_ROUTE, idOrAliasOrEvmAddress) - + var fullApiUrl = MirrorNodeRouter.buildApiUrl( + self.mirrorNodeUrl, MirrorNodeRouter.CONTRACTS_ROUTE, idOrAliasOrEvmAddress) + let responseBody = try await queryFromMirrorNode(fullApiUrl) - + guard let jsonData = responseBody.data(using: .utf8) else { - throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) + throw NSError( + domain: "InvalidResponseError", code: -1, + userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) } - + guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { - throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Response body is not a valid JSON object"]) + throw NSError( + domain: "InvalidResponseError", code: -1, + userInfo: [NSLocalizedDescriptionKey: "Response body is not a valid JSON object"]) } - + return jsonObject } - + internal func getAccountTokens(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { - var fullApiUrl = MirrorNodeRouter.buildApiUrl(self.mirrorNodeUrl, MirrorNodeRouter.ACCOUNTS_ROUTE, idOrAliasOrEvmAddress) - + var fullApiUrl = MirrorNodeRouter.buildApiUrl( + self.mirrorNodeUrl, MirrorNodeRouter.ACCOUNTS_ROUTE, idOrAliasOrEvmAddress) + let responseBody = try await queryFromMirrorNode(fullApiUrl) - + guard let jsonData = responseBody.data(using: .utf8) else { - throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) + throw NSError( + domain: "InvalidResponseError", code: -1, + userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) } - + guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { - throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Response body is not a valid JSON object"]) + throw NSError( + domain: "InvalidResponseError", code: -1, + userInfo: [NSLocalizedDescriptionKey: "Response body is not a valid JSON object"]) } - + return jsonObject } - + internal func queryFromMirrorNode(_ apiUrl: String) async throws -> String { let httpClient = HTTPClient(eventLoopGroupProvider: .singleton) defer { @@ -102,12 +116,10 @@ internal final class MirrorNodeGateway { request.method = .GET let response = 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 } } - - diff --git a/Sources/Hedera/MirrorNodeRouter.swift b/Sources/Hedera/MirrorNodeRouter.swift index 28837049..5bf7c778 100644 --- a/Sources/Hedera/MirrorNodeRouter.swift +++ b/Sources/Hedera/MirrorNodeRouter.swift @@ -18,32 +18,33 @@ * */ +import AsyncHTTPClient import Atomics -import GRPC -import NIOCore import Foundation +import GRPC import HederaProtobufs -import AsyncHTTPClient +import NIOCore -internal final class MirrorNodeRouter { +internal struct MirrorNodeRouter { static let API_VERSION: String = "/api/v1" - + static let LOCAL_NODE_PORT = "5551" - + public static let ACCOUNTS_ROUTE = "accounts" public static let CONTRACTS_ROUTE = "contracts" public static let ACCOUNT_TOKENS_ROUTE = "accounts_tokens" - + static let routes: [String: String] = [ ACCOUNTS_ROUTE: "/accounts/%@", CONTRACTS_ROUTE: "/contracts/%@", ACCOUNT_TOKENS_ROUTE: "/accounts/%@/tokens", ] - + private func MirrorNodeRouter() {} - + static func getMirrorNodeUrl(_ mirrorNetwork: [String], _ ledgerId: LedgerId?) throws -> String { - let mirrorNodeAddress: String? = mirrorNetwork + let mirrorNodeAddress: String? = + mirrorNetwork .map { address in address.prefix { $0 != ":" } } @@ -52,21 +53,19 @@ internal final class MirrorNodeRouter { if mirrorNodeAddress == nil { fatalError("Mirror address not found") } - + var fullMirrorNodeUrl: String - + if ledgerId != nil { fullMirrorNodeUrl = String("http://\(mirrorNodeAddress)") } else { fullMirrorNodeUrl = String("http://\(mirrorNodeAddress):\(LOCAL_NODE_PORT)") } - + return fullMirrorNodeUrl } - + static func buildApiUrl(_ mirrorNodeUrl: String, _ route: String, _ id: String) -> String { return String("\(mirrorNodeUrl)\(API_VERSION)\(String(format: "\(String(describing: routes[route]))", id))") } } - - diff --git a/Sources/Hedera/MirrorNodeService.swift b/Sources/Hedera/MirrorNodeService.swift index 189b2433..bae96f1e 100644 --- a/Sources/Hedera/MirrorNodeService.swift +++ b/Sources/Hedera/MirrorNodeService.swift @@ -18,101 +18,107 @@ * */ +import AsyncHTTPClient import Foundation import HederaProtobufs -import AsyncHTTPClient internal final class MirrorNodeService { internal var mirrorNodeGateway: MirrorNodeGateway - + private init(mirrorNodeGateway: MirrorNodeGateway) { self.mirrorNodeGateway = mirrorNodeGateway } - + internal func getAccountNum(_ evmAddress: String) async throws -> UInt64 { let accountInfoResponse = try await self.mirrorNodeGateway.getAccountInfo(evmAddress) - + guard let accountId = accountInfoResponse["account"] else { - throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error while processing getAccountInfo mirror node query"]) + throw NSError( + domain: "InvalidResponseError", code: -1, + userInfo: [NSLocalizedDescriptionKey: "Error while processing getAccountInfo mirror node query"]) } - + let accountNum = AccountId(String(describing: accountId))?.num - + return accountNum! } - + internal func getAccountEvmAddress(_ num: UInt64) async throws -> EvmAddress { let accountInfoResponse = try await self.mirrorNodeGateway.getAccountInfo(String(describing: num)) - + guard let addressAny = accountInfoResponse["evm_address"] else { - throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error while processing getAccountEvmAddress mirror node query"]) + fatalError("Error while processing getAccountEvmAddress mirror node query") } - + let evmAddress = AccountId(String(describing: addressAny))?.evmAddress - + return evmAddress! } - + internal func getContractNum(_ evmAddress: String) async throws -> UInt64 { let accountInfoResponse = try await self.mirrorNodeGateway.getContractInfo(evmAddress) - + guard let contractId = accountInfoResponse["contract_id"] else { - throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error while processing getAccountInfo mirror node query"]) + fatalError("Error while processing getContractNum mirror node query") } - + let contractIdNum = ContractId(String(describing: contractId))?.num - + return contractIdNum! } - + internal func getTokenBalancesForAccount(_ evmAddress: String) async throws -> [Proto_TokenBalance] { let accountTokensResponse = try await self.mirrorNodeGateway.getAccountTokens(evmAddress) - + guard let tokens = accountTokensResponse["tokens"] else { - throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error while processing getTokenBalancesForAccount mirror node query"]) + fatalError("Error while processing getTokenBalancesForAccount mirror node query") } - + var tokenBalances: [Proto_TokenBalance] = [] - - if let tokensList = tokens as? [[String: Any]] { - tokensList.forEach {token in - let tokenId = TokenId(String(describing: token["token_id"]))?.toProtobuf() - let balance = UInt64(String(describing: token["balance"])) - let decimals = UInt32(String(describing: token["decimals"])) - - let tokenBalanceProto = Proto_TokenBalance.with { proto in - proto.tokenID = tokenId! - proto.balance = balance! - proto.decimals = decimals! - } - - tokenBalances.append(tokenBalanceProto) + + guard let tokensList = tokens as? [[String: Any]] else { + throw NSError( + domain: "InvalidResponseError", code: -1, + userInfo: [ + NSLocalizedDescriptionKey: "Error while processing getTokenBalancesForAccount mirror node query" + ]) + } + tokensList.forEach { token in + let tokenId = TokenId(String(describing: token["token_id"]))?.toProtobuf() + let balance = UInt64(String(describing: token["balance"])) + let decimals = UInt32(String(describing: token["decimals"])) + + let tokenBalanceProto = Proto_TokenBalance.with { proto in + proto.tokenID = tokenId! + proto.balance = balance! + proto.decimals = decimals! } - } else { - throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error while processing getTokenBalancesForAccount mirror node query"]) + + tokenBalances.append(tokenBalanceProto) } - + return tokenBalances } - + internal func getTokenRelationshipsForAccount(_ evmAddress: String) async throws -> [Proto_TokenRelationship] { let accountTokensResponse = try await self.mirrorNodeGateway.getAccountTokens(evmAddress) - + guard let tokens = accountTokensResponse["tokens"] else { - throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error while processing getTokenRelationshipsForAccount mirror node query"]) + fatalError("Error while processing getTokenRelationshipsForAccount mirror node query") + } - + var tokenBalances: [Proto_TokenRelationship] = [] - + if let tokensList = tokens as? [[String: Any]] { - try tokensList.forEach {token in + try tokensList.forEach { token in let tokenId = TokenId(String(describing: token["token_id"]))?.toProtobuf() let balance = UInt64(String(describing: token["balance"])) let decimals = UInt32(String(describing: token["decimals"])) let kycStatus = String(describing: token["kyc_status"]) let freezeStatus = String(describing: token["freeze_status"]) let automaticAssociation = Bool(String(describing: token["automatic_assocation"])) - + let tokenRelationshipsProto = try Proto_TokenRelationship.with { proto in proto.tokenID = tokenId! proto.balance = balance! @@ -121,32 +127,32 @@ internal final class MirrorNodeService { proto.freezeStatus = try getTokenFreezeStatusFromString(freezeStatus) proto.automaticAssociation = automaticAssociation! } - + tokenBalances.append(tokenRelationshipsProto) } } else { - throw NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Error while processing getTokenRelationshipsForAccount mirror node query"]) + fatalError("Error while processing getTokenRelationshipsForAccount mirror node query") } - + return tokenBalances } - + 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 NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid token KYC status: \(tokenKycStatusString)"]) + case _: fatalError("Invalid token KYC status: \(tokenKycStatusString)") } } - + 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 NSError(domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid token freeze status: \(tokenFreezeStatusString)"]) + case _: fatalError("Invalid token Freeze status: \(tokenFreezeStatusString)") } } - + } From d00e87f8613e8274123f559e6a07fb3bd0a588a0 Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Tue, 28 May 2024 20:33:43 -0700 Subject: [PATCH 05/18] feat: add token relationship Signed-off-by: Ricky Saechao --- Sources/Hedera/Token/TokenRelationship.swift | 141 +++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 Sources/Hedera/Token/TokenRelationship.swift diff --git a/Sources/Hedera/Token/TokenRelationship.swift b/Sources/Hedera/Token/TokenRelationship.swift new file mode 100644 index 00000000..79354cef --- /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 { + /// 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: Bool? + + /// The Freeze status of the account (FreezeNotApplicable, Frozen or Unfrozen). + /// + /// If the token does not have Freeze key, FreezeNotApplicable is returned + public let freezeStatus: Bool? + + /// 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: Bool?, freezeStatus: Bool?, + 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: Bool? + var kycStatus: Bool? + + switch proto.freezeStatus { + case .freezeNotApplicable: + freezeStatus = nil + case .frozen: + freezeStatus = true + case .unfrozen: + freezeStatus = false + case .unrecognized(_): + fatalError("Unrecognized Freeze Status from Protobuf: \(proto.freezeStatus)") + } + + switch proto.kycStatus { + case .kycNotApplicable: + kycStatus = nil + case .granted: + kycStatus = true + case .revoked: + kycStatus = false + case .unrecognized(_): + fatalError("Unrecognized 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 true: + protoFreezeStatus = Proto_TokenFreezeStatus.frozen + case false: + protoFreezeStatus = Proto_TokenFreezeStatus.unfrozen + case nil: + protoFreezeStatus = Proto_TokenFreezeStatus.freezeNotApplicable + case .some(_): + fatalError("Unrecognized Freeze Status") + } + + switch kycStatus { + case true: + protoKycStatus = Proto_TokenKycStatus.granted + case false: + protoKycStatus = Proto_TokenKycStatus.revoked + case nil: + protoKycStatus = Proto_TokenKycStatus.kycNotApplicable + case .some(_): + fatalError("Unrecognized KYC Status") + } + + proto.tokenID = tokenId.toProtobuf() + proto.balance = balance + proto.symbol = symbol + proto.freezeStatus = protoFreezeStatus + proto.kycStatus = protoKycStatus + proto.automaticAssociation = automaticAssociation + } + } +} From d11ac317917728f054f841a93dbaf7cf201b774d Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Wed, 29 May 2024 07:39:45 -0700 Subject: [PATCH 06/18] feat: add extra context for mirrorNetworkNode and LedgerId Signed-off-by: Ricky Saechao --- .../Hedera/Account/AccountBalanceQuery.swift | 4 ++- Sources/Hedera/Account/AccountInfoQuery.swift | 4 ++- .../Hedera/Account/AccountRecordsQuery.swift | 4 ++- .../Hedera/Account/AccountStakersQuery.swift | 4 ++- Sources/Hedera/ChunkedTransaction.swift | 4 +-- .../Contract/ContractBytecodeQuery.swift | 4 ++- .../Hedera/Contract/ContractCallQuery.swift | 4 ++- .../Hedera/Contract/ContractInfoQuery.swift | 20 ++++++++++-- Sources/Hedera/Execute.swift | 13 +++++--- Sources/Hedera/File/FileContentsQuery.swift | 4 ++- Sources/Hedera/File/FileInfoQuery.swift | 4 ++- Sources/Hedera/MirrorNodeGateway.swift | 6 ++-- Sources/Hedera/MirrorNodeService.swift | 2 +- Sources/Hedera/NetworkVersionInfoQuery.swift | 4 ++- Sources/Hedera/PingQuery.swift | 4 ++- Sources/Hedera/Query.swift | 31 ++++++++++++++----- Sources/Hedera/QueryCost.swift | 4 ++- .../Hedera/Schedule/ScheduleInfoQuery.swift | 4 ++- Sources/Hedera/Token/TokenInfoQuery.swift | 4 ++- Sources/Hedera/Token/TokenNftInfoQuery.swift | 4 ++- Sources/Hedera/Token/TokenRelationship.swift | 2 +- Sources/Hedera/Topic/TopicInfoQuery.swift | 4 ++- Sources/Hedera/Transaction.swift | 2 +- .../Transaction/TransactionSources.swift | 2 +- Sources/Hedera/TransactionReceiptQuery.swift | 4 ++- Sources/Hedera/TransactionRecordQuery.swift | 4 ++- 26 files changed, 109 insertions(+), 41 deletions(-) diff --git a/Sources/Hedera/Account/AccountBalanceQuery.swift b/Sources/Hedera/Account/AccountBalanceQuery.swift index c2ba5953..d05eb1d2 100644 --- a/Sources/Hedera/Account/AccountBalanceQuery.swift +++ b/Sources/Hedera/Account/AccountBalanceQuery.swift @@ -85,7 +85,9 @@ public final class AccountBalanceQuery: Query { try await Proto_CryptoServiceAsyncClient(channel: channel).cryptoGetBalance(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 .cryptogetAccountBalance(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `cryptogetAccountBalance`") } diff --git a/Sources/Hedera/Account/AccountInfoQuery.swift b/Sources/Hedera/Account/AccountInfoQuery.swift index b1b91b16..98e2bc3c 100644 --- a/Sources/Hedera/Account/AccountInfoQuery.swift +++ b/Sources/Hedera/Account/AccountInfoQuery.swift @@ -59,7 +59,9 @@ public final class AccountInfoQuery: Query { try await Proto_CryptoServiceAsyncClient(channel: channel).getAccountInfo(request) } - internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response { + internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) throws + -> Response + { guard case .cryptoGetInfo(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `cryptoGetInfo`") } 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..10b7789b 100644 --- a/Sources/Hedera/ChunkedTransaction.swift +++ b/Sources/Hedera/ChunkedTransaction.swift @@ -206,7 +206,7 @@ extension ChunkedTransaction.FirstChunkView: Execute { self.transaction.regenerateTransactionId } - internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> ( + internal func makeRequest(_ client: Client, _ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> ( GrpcRequest, Context ) { assert(transaction.isFrozen) @@ -265,7 +265,7 @@ extension ChunkedTransaction.ChunkView: Execute { self.transaction.regenerateTransactionId } - internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> ( + internal func makeRequest(_ client: Client, _ 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/ContractInfoQuery.swift b/Sources/Hedera/Contract/ContractInfoQuery.swift index b0f02441..0c1d4ab4 100644 --- a/Sources/Hedera/Contract/ContractInfoQuery.swift +++ b/Sources/Hedera/Contract/ContractInfoQuery.swift @@ -42,6 +42,7 @@ public final class ContractInfoQuery: Query { } internal override func toQueryProtobufWith(_ header: Proto_QueryHeader) -> Proto_Query { + .with { proto in proto.contractGetInfo = .with { proto in proto.header = header @@ -54,8 +55,21 @@ 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) + + switch response { + case .contractGetInfo(let proto): + let contractId = try ContractId(protobuf: proto.contractInfo.contractID) + let tokenRelationships = try await mirrorNodeService.getTokenRelationshipsForAccount(String(describing: contractId.num)) + + let consensusProto = proto.contractInfo + let updatedProto = consensusProto. + + default: throw HError.fromProtobuf("unexpected \(response) received, expected `contractGetInfo`") } @@ -66,5 +80,5 @@ public final class ContractInfoQuery: Query { try contractId?.validateChecksums(on: ledgerId) try super.validateChecksums(on: ledgerId) } - } + diff --git a/Sources/Hedera/Execute.swift b/Sources/Hedera/Execute.swift index 7a7bddcd..21551896 100644 --- a/Sources/Hedera/Execute.swift +++ b/Sources/Hedera/Execute.swift @@ -60,7 +60,9 @@ 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(_ client: Client, _ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> ( + GrpcRequest, Context + ) func execute(_ channel: GRPCChannel, _ request: GrpcRequest) async throws -> GrpcResponse @@ -71,7 +73,7 @@ internal protocol Execute { _ context: Context, _ nodeAccountId: AccountId, _ transactionId: TransactionId? - ) throws -> Response + ) async throws -> Response func makeErrorPrecheck(_ status: Status, _ transactionId: TransactionId?) -> HError @@ -95,6 +97,7 @@ private struct ExecuteContext { fileprivate let network: Network fileprivate let backoffConfig: LegacyExponentialBackoff fileprivate let maxAttempts: Int + fileprivate let client: Client // timeout for a single grpc request. fileprivate let grpcTimeout: Duration? } @@ -150,6 +153,7 @@ internal func executeAny( network: client.net, backoffConfig: backoffBuilder, maxAttempts: backoff.maxAttempts, + client: client, grpcTimeout: nil ), executable: executable) @@ -185,7 +189,7 @@ 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.client, transactionId, nodeAccountId) let response: E.GrpcResponse do { @@ -222,7 +226,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 +313,7 @@ private struct NodeIndexesGeneratorMap: AsyncSequence, AsyncIteratorProtocol { network: ctx.network, backoffConfig: ctx.backoffConfig, maxAttempts: ctx.maxAttempts, + client: ctx.client, 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/MirrorNodeGateway.swift b/Sources/Hedera/MirrorNodeGateway.swift index 503eb452..2f7f4e68 100644 --- a/Sources/Hedera/MirrorNodeGateway.swift +++ b/Sources/Hedera/MirrorNodeGateway.swift @@ -44,7 +44,7 @@ internal struct MirrorNodeGateway { } internal func getAccountInfo(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { - var fullApiUrl = MirrorNodeRouter.buildApiUrl( + let fullApiUrl = MirrorNodeRouter.buildApiUrl( self.mirrorNodeUrl, MirrorNodeRouter.ACCOUNTS_ROUTE, idOrAliasOrEvmAddress) let responseBody = try await queryFromMirrorNode(fullApiUrl) @@ -65,7 +65,7 @@ internal struct MirrorNodeGateway { } internal func getContractInfo(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { - var fullApiUrl = MirrorNodeRouter.buildApiUrl( + let fullApiUrl = MirrorNodeRouter.buildApiUrl( self.mirrorNodeUrl, MirrorNodeRouter.CONTRACTS_ROUTE, idOrAliasOrEvmAddress) let responseBody = try await queryFromMirrorNode(fullApiUrl) @@ -86,7 +86,7 @@ internal struct MirrorNodeGateway { } internal func getAccountTokens(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { - var fullApiUrl = MirrorNodeRouter.buildApiUrl( + let fullApiUrl = MirrorNodeRouter.buildApiUrl( self.mirrorNodeUrl, MirrorNodeRouter.ACCOUNTS_ROUTE, idOrAliasOrEvmAddress) let responseBody = try await queryFromMirrorNode(fullApiUrl) diff --git a/Sources/Hedera/MirrorNodeService.swift b/Sources/Hedera/MirrorNodeService.swift index bae96f1e..5a50d094 100644 --- a/Sources/Hedera/MirrorNodeService.swift +++ b/Sources/Hedera/MirrorNodeService.swift @@ -25,7 +25,7 @@ import HederaProtobufs internal final class MirrorNodeService { internal var mirrorNodeGateway: MirrorNodeGateway - private init(mirrorNodeGateway: MirrorNodeGateway) { + init(_ mirrorNodeGateway: MirrorNodeGateway) { self.mirrorNodeGateway = mirrorNodeGateway } 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..a2e4040b 100644 --- a/Sources/Hedera/PingQuery.swift +++ b/Sources/Hedera/PingQuery.swift @@ -73,7 +73,9 @@ extension PingQuery: Execute { nil } - internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> (Proto_Query, ()) { + internal func makeRequest(_ client: Client, _ 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..048955c4 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,24 @@ extension Query: Execute { payment.index } - internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> (Proto_Query, ()) { + internal func makeRequest(_ client: Client, _ 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(client, transactionId, nodeAccountId).0 } }) - return (request, ()) + let context = MirrorNetworkContext( + ledgerId: client.ledgerId, + mirrorNetworkNodes: client.mirrorNetwork + ) + + return (request, context) } internal func execute(_ channel: GRPC.GRPCChannel, _ request: Proto_Query) async throws -> Proto_Response { @@ -235,13 +249,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..8bde5c7d 100644 --- a/Sources/Hedera/QueryCost.swift +++ b/Sources/Hedera/QueryCost.swift @@ -67,7 +67,9 @@ extension QueryCost: Execute { internal var requiresTransactionId: Bool { false } - internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> (Proto_Query, ()) { + internal func makeRequest(_ client: Client, _ 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 index 79354cef..c5ae1735 100644 --- a/Sources/Hedera/Token/TokenRelationship.swift +++ b/Sources/Hedera/Token/TokenRelationship.swift @@ -25,7 +25,7 @@ import HederaProtobufs /// /// See Hedera /// Documentation -public struct TokenRelationship { +public struct TokenRelationship: Sendable { /// The ID of the token public let tokenId: TokenId 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..bc84033a 100644 --- a/Sources/Hedera/Transaction.swift +++ b/Sources/Hedera/Transaction.swift @@ -593,7 +593,7 @@ extension Transaction: Execute { self.operator?.accountId } - internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> ( + internal func makeRequest(_ client: Client, _ 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..cc516120 100644 --- a/Sources/Hedera/Transaction/TransactionSources.swift +++ b/Sources/Hedera/Transaction/TransactionSources.swift @@ -428,7 +428,7 @@ extension SourceTransactionExecuteView: Execute { nil } - internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> ( + internal func makeRequest(_ client: Client, _ 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`") } From 5060e8020197fdf424b114a6035fde6f6ef32eb4 Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Wed, 29 May 2024 22:48:42 -0700 Subject: [PATCH 07/18] chore: update protobufs Signed-off-by: Ricky Saechao --- protobufs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protobufs b/protobufs index 17fb1486..43d9159b 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 17fb148652b1e2badda1c5cdff531f0fcf7f4c55 +Subproject commit 43d9159b4f2302a681ba9923cb849b0d8c9a4526 From dc384b6ba0e1377216c0c58093ceb0122c03b3e4 Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Thu, 30 May 2024 13:43:14 -0700 Subject: [PATCH 08/18] chore: update protobufs Signed-off-by: Ricky Saechao --- protobufs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protobufs b/protobufs index 43d9159b..6096d432 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 43d9159b4f2302a681ba9923cb849b0d8c9a4526 +Subproject commit 6096d432efcb9bc76bb0b813cf17c1b001e36335 From 86f519b1c9534416f98e72959aae1e79b45fcc6b Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Thu, 30 May 2024 13:44:03 -0700 Subject: [PATCH 09/18] feat: modify responses to use tokenRelationships Signed-off-by: Ricky Saechao --- Sources/Hedera/Account/AccountInfo.swift | 11 ++++ Sources/Hedera/Account/AccountInfoQuery.swift | 39 ++++++++++++- Sources/Hedera/Contract/ContractId.swift | 12 ++++ Sources/Hedera/Contract/ContractInfo.swift | 12 ++++ .../Hedera/Contract/ContractInfoQuery.swift | 43 +++++++++----- Sources/Hedera/MirrorNodeGateway.swift | 15 ++--- Sources/Hedera/MirrorNodeRouter.swift | 33 +++++------ Sources/Hedera/MirrorNodeService.swift | 5 +- .../Contract/ContractIdPopulation.swift | 57 +++++++++++++++++++ .../Contract/ContractInfo.swift | 2 +- 10 files changed, 186 insertions(+), 43 deletions(-) create mode 100644 Tests/HederaE2ETests/Contract/ContractIdPopulation.swift diff --git a/Sources/Hedera/Account/AccountInfo.swift b/Sources/Hedera/Account/AccountInfo.swift index 05165e18..18e99e19 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? + /// Staking metadata 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 98e2bc3c..7bce0179 100644 --- a/Sources/Hedera/Account/AccountInfoQuery.swift +++ b/Sources/Hedera/Account/AccountInfoQuery.swift @@ -59,14 +59,49 @@ public final class AccountInfoQuery: Query { try await Proto_CryptoServiceAsyncClient(channel: channel).getAccountInfo(request) } - internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) throws + 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(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `cryptoGetInfo`") } - return try .fromProtobuf(proto.accountInfo) + let accountInfoProto = proto.accountInfo + let accountId = try AccountId.fromProtobuf(accountInfoProto.accountID) + let tokenRelationshipsProto = try await mirrorNodeService.getTokenRelationshipsForAccount( + String(describing: accountId.num)) + + var tokenRelationships: [TokenId: TokenRelationship] = [:] + + for relationship in tokenRelationshipsProto { + tokenRelationships[.fromProtobuf(relationship.tokenID)] = try TokenRelationship.fromProtobuf(relationship) + } + + return AccountInfo( + accountId: try AccountId.fromProtobuf(accountInfoProto.accountID), + contractAccountId: accountInfoProto.contractAccountID, + isDeleted: accountInfoProto.deleted, + proxyAccountId: try .fromProtobuf(accountInfoProto.proxyAccountID), + proxyReceived: Hbar.fromTinybars(accountInfoProto.proxyReceived), + key: try .fromProtobuf(accountInfoProto.key), + balance: .fromTinybars(Int64(accountInfoProto.balance)), + sendRecordThreshold: Hbar.fromTinybars(Int64(accountInfoProto.generateSendRecordThreshold)), + receiveRecordThreshold: Hbar.fromTinybars(Int64(accountInfoProto.generateReceiveRecordThreshold)), + isReceiverSignatureRequired: accountInfoProto.receiverSigRequired, + expirationTime: .fromProtobuf(accountInfoProto.expirationTime), + autoRenewPeriod: .fromProtobuf(accountInfoProto.autoRenewPeriod), + accountMemo: accountInfoProto.memo, + ownedNfts: UInt64(accountInfoProto.ownedNfts), + maxAutomaticTokenAssociations: UInt32(accountInfoProto.maxAutomaticTokenAssociations), + aliasKey: try .fromAliasBytes(accountInfoProto.alias), + ethereumNonce: UInt64(accountInfoProto.ethereumNonce), + tokenRelationships: tokenRelationships, + ledgerId: .fromBytes(accountInfoProto.ledgerID), + staking: try .fromProtobuf(accountInfoProto.stakingInfo) + ) } internal override func validateChecksums(on ledgerId: LedgerId) throws { diff --git a/Sources/Hedera/Contract/ContractId.swift b/Sources/Hedera/Contract/ContractId.swift index e1d83d99..4f204132 100644 --- a/Sources/Hedera/Contract/ContractId.swift +++ b/Sources/Hedera/Contract/ContractId.swift @@ -119,6 +119,18 @@ public struct ContractId: EntityId { public func toBytes() -> Data { toProtobufBytes() } + + public func populateContractNum(_ client: Client) async throws -> Self { + let address = try EvmAddress.fromBytes(self.evmAddress!) + + let mirrorNodeGateway = try MirrorNodeGateway.forClient(client) + let mirrorNodeService = MirrorNodeService(mirrorNodeGateway) + + let contractNum = try await mirrorNodeService.getContractNum(address.toString()) + + return Self(shard: shard, realm: realm, num: contractNum) + } + } #if compiler(>=5.7) diff --git a/Sources/Hedera/Contract/ContractInfo.swift b/Sources/Hedera/Contract/ContractInfo.swift index b0393883..6d7275eb 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 + /// + /// Query mirror node + 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 0c1d4ab4..6e6a1d17 100644 --- a/Sources/Hedera/Contract/ContractInfoQuery.swift +++ b/Sources/Hedera/Contract/ContractInfoQuery.swift @@ -55,25 +55,43 @@ public final class ContractInfoQuery: Query { try await Proto_SmartContractServiceAsyncClient(channel: channel).getContractInfo(request) } - internal override func makeQueryResponse(_ context: MirrorNetworkContext, _ response: Proto_Response.OneOf_Response) async throws + 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) - - switch response { - case .contractGetInfo(let proto): - let contractId = try ContractId(protobuf: proto.contractInfo.contractID) - let tokenRelationships = try await mirrorNodeService.getTokenRelationshipsForAccount(String(describing: contractId.num)) - - let consensusProto = proto.contractInfo - let updatedProto = consensusProto. - - default: + + guard case .contractGetInfo(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `contractGetInfo`") } + let contractInfoProto = proto.contractInfo + let contractId = try ContractId.fromProtobuf(contractInfoProto.contractID) + let tokenRelationshipsProto = try await mirrorNodeService.getTokenRelationshipsForAccount( + String(describing: contractId.num)) + + var tokenRelationships: [TokenId: TokenRelationship] = [:] - return try .fromProtobuf(proto.contractInfo) + for relationship in tokenRelationshipsProto { + tokenRelationships[.fromProtobuf(relationship.tokenID)] = try TokenRelationship.fromProtobuf(relationship) + } + + return ContractInfo( + contractId: try ContractId.fromProtobuf(contractInfoProto.contractID), + accountId: try AccountId.fromProtobuf(contractInfoProto.accountID), + contractAccountId: contractInfoProto.contractAccountID, + adminKey: try .fromProtobuf(contractInfoProto.adminKey), + expirationTime: .fromProtobuf(contractInfoProto.expirationTime), + autoRenewPeriod: .fromProtobuf(contractInfoProto.autoRenewPeriod), + storage: UInt64(contractInfoProto.storage), + contractMemo: contractInfoProto.memo, + balance: .fromTinybars(Int64(contractInfoProto.balance)), + isDeleted: contractInfoProto.deleted, + autoRenewAccountId: try .fromProtobuf(contractInfoProto.autoRenewAccountID), + maxAutomaticTokenAssociations: UInt32(contractInfoProto.maxAutomaticTokenAssociations), + tokenRelationships: tokenRelationships, + ledgerId: .fromBytes(contractInfoProto.ledgerID), + stakingInfo: try .fromProtobuf(contractInfoProto.stakingInfo)) } internal override func validateChecksums(on ledgerId: LedgerId) throws { @@ -81,4 +99,3 @@ public final class ContractInfoQuery: Query { try super.validateChecksums(on: ledgerId) } } - diff --git a/Sources/Hedera/MirrorNodeGateway.swift b/Sources/Hedera/MirrorNodeGateway.swift index 2f7f4e68..16aef38f 100644 --- a/Sources/Hedera/MirrorNodeGateway.swift +++ b/Sources/Hedera/MirrorNodeGateway.swift @@ -31,7 +31,7 @@ internal struct MirrorNodeGateway { self.mirrorNodeUrl = mirrorNodeUrl } - internal static func forClient(client: Client) throws -> MirrorNodeGateway { + internal static func forClient(_ client: Client) throws -> MirrorNodeGateway { let mirrorNodeUrl = try MirrorNodeRouter.getMirrorNodeUrl(client.mirrorNetwork, client.ledgerId) return .init(mirrorNodeUrl: mirrorNodeUrl) @@ -45,7 +45,7 @@ internal struct MirrorNodeGateway { internal func getAccountInfo(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { let fullApiUrl = MirrorNodeRouter.buildApiUrl( - self.mirrorNodeUrl, MirrorNodeRouter.ACCOUNTS_ROUTE, idOrAliasOrEvmAddress) + self.mirrorNodeUrl, MirrorNodeRouter.accountsRoute, idOrAliasOrEvmAddress) let responseBody = try await queryFromMirrorNode(fullApiUrl) @@ -66,7 +66,9 @@ internal struct MirrorNodeGateway { internal func getContractInfo(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { let fullApiUrl = MirrorNodeRouter.buildApiUrl( - self.mirrorNodeUrl, MirrorNodeRouter.CONTRACTS_ROUTE, idOrAliasOrEvmAddress) + self.mirrorNodeUrl, MirrorNodeRouter.contractsRoute, idOrAliasOrEvmAddress) + + print("ContractfullApiUrl: \(fullApiUrl)") let responseBody = try await queryFromMirrorNode(fullApiUrl) @@ -75,7 +77,6 @@ internal struct MirrorNodeGateway { domain: "InvalidResponseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) } - guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { throw NSError( domain: "InvalidResponseError", code: -1, @@ -87,10 +88,10 @@ internal struct MirrorNodeGateway { internal func getAccountTokens(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { let fullApiUrl = MirrorNodeRouter.buildApiUrl( - self.mirrorNodeUrl, MirrorNodeRouter.ACCOUNTS_ROUTE, idOrAliasOrEvmAddress) - + self.mirrorNodeUrl, MirrorNodeRouter.accountTokensRoute, idOrAliasOrEvmAddress) + let responseBody = try await queryFromMirrorNode(fullApiUrl) - + guard let jsonData = responseBody.data(using: .utf8) else { throw NSError( domain: "InvalidResponseError", code: -1, diff --git a/Sources/Hedera/MirrorNodeRouter.swift b/Sources/Hedera/MirrorNodeRouter.swift index 5bf7c778..f671964e 100644 --- a/Sources/Hedera/MirrorNodeRouter.swift +++ b/Sources/Hedera/MirrorNodeRouter.swift @@ -26,46 +26,43 @@ import HederaProtobufs import NIOCore internal struct MirrorNodeRouter { - static let API_VERSION: String = "/api/v1" + static let apiVersion: String = "/api/v1" - static let LOCAL_NODE_PORT = "5551" + static let localNodePort = "5551" - public static let ACCOUNTS_ROUTE = "accounts" - public static let CONTRACTS_ROUTE = "contracts" - public static let ACCOUNT_TOKENS_ROUTE = "accounts_tokens" + public static let accountsRoute = "accounts" + public static let contractsRoute = "contracts" + public static let accountTokensRoute = "account_tokens" static let routes: [String: String] = [ - ACCOUNTS_ROUTE: "/accounts/%@", - CONTRACTS_ROUTE: "/contracts/%@", - ACCOUNT_TOKENS_ROUTE: "/accounts/%@/tokens", + accountsRoute: "/accounts/%@", + contractsRoute: "/contracts/%@", + accountTokensRoute: "/accounts/%@/tokens", ] private func MirrorNodeRouter() {} static func getMirrorNodeUrl(_ mirrorNetwork: [String], _ ledgerId: LedgerId?) throws -> String { - let mirrorNodeAddress: String? = - mirrorNetwork + var mirrorNodeAddress: String = "" + + mirrorNetwork .map { address in address.prefix { $0 != ":" } } - .first.map { String($0) } - - if mirrorNodeAddress == nil { - fatalError("Mirror address not found") - } + .first.map { mirrorNodeAddress = String($0) }! var fullMirrorNodeUrl: String if ledgerId != nil { - fullMirrorNodeUrl = String("http://\(mirrorNodeAddress)") + fullMirrorNodeUrl = String("https://\(mirrorNodeAddress)") } else { - fullMirrorNodeUrl = String("http://\(mirrorNodeAddress):\(LOCAL_NODE_PORT)") + fullMirrorNodeUrl = String("http://\(mirrorNodeAddress):\(localNodePort)") } return fullMirrorNodeUrl } static func buildApiUrl(_ mirrorNodeUrl: String, _ route: String, _ id: String) -> String { - return String("\(mirrorNodeUrl)\(API_VERSION)\(String(format: "\(String(describing: routes[route]))", id))") + return String("\(mirrorNodeUrl)\(apiVersion)\(String(format: "\(String(describing: routes[route]))", id))!") } } diff --git a/Sources/Hedera/MirrorNodeService.swift b/Sources/Hedera/MirrorNodeService.swift index 5a50d094..d2f300c3 100644 --- a/Sources/Hedera/MirrorNodeService.swift +++ b/Sources/Hedera/MirrorNodeService.swift @@ -64,6 +64,8 @@ internal final class MirrorNodeService { let contractIdNum = ContractId(String(describing: contractId))?.num + fatalError("contract id: \(contractIdNum)") + return contractIdNum! } @@ -104,8 +106,7 @@ internal final class MirrorNodeService { let accountTokensResponse = try await self.mirrorNodeGateway.getAccountTokens(evmAddress) guard let tokens = accountTokensResponse["tokens"] else { - fatalError("Error while processing getTokenRelationshipsForAccount mirror node query") - + fatalError("Error while processing getTokenRelationshipsForAccount mirror node query: \(accountTokensResponse)") } var tokenBalances: [Proto_TokenRelationship] = [] diff --git a/Tests/HederaE2ETests/Contract/ContractIdPopulation.swift b/Tests/HederaE2ETests/Contract/ContractIdPopulation.swift new file mode 100644 index 00000000..95854b94 --- /dev/null +++ b/Tests/HederaE2ETests/Contract/ContractIdPopulation.swift @@ -0,0 +1,57 @@ +/* + * ‌ + * 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 Hedera +import XCTest + +internal final class ContractIdPopulation: XCTestCase { + internal let contractByteCode = "608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506101cb806100606000396000f3fe608060405260043610610046576000357c01000000000000000000000000000000000000000000000000000000009004806341c0e1b51461004b578063cfae321714610062575b600080fd5b34801561005757600080fd5b506100606100f2565b005b34801561006e57600080fd5b50610077610162565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100b757808201518184015260208101905061009c565b50505050905090810190601f1680156100e45780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610160573373ffffffffffffffffffffffffffffffffffffffff16ff5b565b60606040805190810160405280600d81526020017f48656c6c6f2c20776f726c64210000000000000000000000000000000000000081525090509056fea165627a7a72305820ae96fb3af7cde9c0abfe365272441894ab717f816f07f41f07b1cbede54e256e0029".data(using: .utf8)! + + internal func testPopulateContractIdNum() async throws { + let testEnv = try TestEnvironment.nonFree + + let fileCreateReceipt = try await FileCreateTransaction() + .keys([.single(testEnv.operator.privateKey.publicKey)]) + .contents(self.contractByteCode) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + let fileId = try XCTUnwrap(fileCreateReceipt.fileId) + + let contractCreateReceipt = try await ContractCreateTransaction() + .adminKey(.single(testEnv.operator.privateKey.publicKey)) + .gas(100000) + .constructorParameters(ContractFunctionParameters().addString("Hello from Hedera.")) + .contractMemo("[e2e::ContractIdPopulation]") + .bytecodeFileId(fileId) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + let contractId = try XCTUnwrap(contractCreateReceipt.contractId) + + let contractInfo = try await ContractInfoQuery(contractId: contractId).execute(testEnv.client) + + let contractIdMirror = try ContractId.fromEvmAddress(0, 0, contractInfo.contractAccountId) + + let newContractId = try await contractIdMirror.populateContractNum(testEnv.client) + + XCTAssertEqual(contractId.num, newContractId.num) + } +} \ No newline at end of file diff --git a/Tests/HederaE2ETests/Contract/ContractInfo.swift b/Tests/HederaE2ETests/Contract/ContractInfo.swift index 6f37b56c..9093cd68 100644 --- a/Tests/HederaE2ETests/Contract/ContractInfo.swift +++ b/Tests/HederaE2ETests/Contract/ContractInfo.swift @@ -22,7 +22,7 @@ import Hedera import XCTest internal final class ContractInfo: XCTestCase { - internal func testQuery() async throws { + internal func testQueryBoo() async throws { let testEnv = try TestEnvironment.nonFree let contractId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) From 0b19af36064b0182308edb322f905df8e4fa9fb1 Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Fri, 31 May 2024 16:51:57 -0700 Subject: [PATCH 10/18] fix: correct formatting for api url Signed-off-by: Ricky Saechao --- Sources/Hedera/MirrorNodeGateway.swift | 11 ++++------- Sources/Hedera/MirrorNodeRouter.swift | 2 +- Sources/Hedera/MirrorNodeService.swift | 9 ++++++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/Hedera/MirrorNodeGateway.swift b/Sources/Hedera/MirrorNodeGateway.swift index 16aef38f..6fa0c6ef 100644 --- a/Sources/Hedera/MirrorNodeGateway.swift +++ b/Sources/Hedera/MirrorNodeGateway.swift @@ -68,8 +68,6 @@ internal struct MirrorNodeGateway { let fullApiUrl = MirrorNodeRouter.buildApiUrl( self.mirrorNodeUrl, MirrorNodeRouter.contractsRoute, idOrAliasOrEvmAddress) - print("ContractfullApiUrl: \(fullApiUrl)") - let responseBody = try await queryFromMirrorNode(fullApiUrl) guard let jsonData = responseBody.data(using: .utf8) else { @@ -89,9 +87,9 @@ internal struct MirrorNodeGateway { internal func getAccountTokens(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { let fullApiUrl = MirrorNodeRouter.buildApiUrl( self.mirrorNodeUrl, MirrorNodeRouter.accountTokensRoute, idOrAliasOrEvmAddress) - + let responseBody = try await queryFromMirrorNode(fullApiUrl) - + guard let jsonData = responseBody.data(using: .utf8) else { throw NSError( domain: "InvalidResponseError", code: -1, @@ -113,10 +111,9 @@ internal struct MirrorNodeGateway { try? httpClient.syncShutdown() } - var request = HTTPClientRequest(url: apiUrl) - request.method = .GET + let request = HTTPClientRequest(url: apiUrl) - let response = try await httpClient.execute(request, timeout: .seconds(30)) + 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) diff --git a/Sources/Hedera/MirrorNodeRouter.swift b/Sources/Hedera/MirrorNodeRouter.swift index f671964e..9308f0b2 100644 --- a/Sources/Hedera/MirrorNodeRouter.swift +++ b/Sources/Hedera/MirrorNodeRouter.swift @@ -63,6 +63,6 @@ internal struct MirrorNodeRouter { } static func buildApiUrl(_ mirrorNodeUrl: String, _ route: String, _ id: String) -> String { - return String("\(mirrorNodeUrl)\(apiVersion)\(String(format: "\(String(describing: routes[route]))", id))!") + return String("\(mirrorNodeUrl)\(apiVersion)\(String(format: "\(String(describing: routes[route]!))", id))") } } diff --git a/Sources/Hedera/MirrorNodeService.swift b/Sources/Hedera/MirrorNodeService.swift index d2f300c3..9f0bf473 100644 --- a/Sources/Hedera/MirrorNodeService.swift +++ b/Sources/Hedera/MirrorNodeService.swift @@ -64,8 +64,6 @@ internal final class MirrorNodeService { let contractIdNum = ContractId(String(describing: contractId))?.num - fatalError("contract id: \(contractIdNum)") - return contractIdNum! } @@ -106,7 +104,12 @@ internal final class MirrorNodeService { let accountTokensResponse = try await self.mirrorNodeGateway.getAccountTokens(evmAddress) guard let tokens = accountTokensResponse["tokens"] else { - fatalError("Error while processing getTokenRelationshipsForAccount mirror node query: \(accountTokensResponse)") + throw NSError( + domain: "InvalidResponseError", code: -1, + userInfo: [ + NSLocalizedDescriptionKey: + "Error while processing getTokenRelationshipsForAccount mirror node query" + ]) } var tokenBalances: [Proto_TokenRelationship] = [] From 0e69fbb21328bf3af1120f3e50119f154196accb Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Fri, 31 May 2024 16:52:46 -0700 Subject: [PATCH 11/18] feat: add contract id population test Signed-off-by: Ricky Saechao --- Sources/Hedera/Contract/ContractId.swift | 3 +-- .../Contract/ContractIdPopulation.swift | 12 +++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Sources/Hedera/Contract/ContractId.swift b/Sources/Hedera/Contract/ContractId.swift index 4f204132..c0cdadec 100644 --- a/Sources/Hedera/Contract/ContractId.swift +++ b/Sources/Hedera/Contract/ContractId.swift @@ -125,12 +125,11 @@ public struct ContractId: EntityId { let mirrorNodeGateway = try MirrorNodeGateway.forClient(client) let mirrorNodeService = MirrorNodeService(mirrorNodeGateway) - + let contractNum = try await mirrorNodeService.getContractNum(address.toString()) return Self(shard: shard, realm: realm, num: contractNum) } - } #if compiler(>=5.7) diff --git a/Tests/HederaE2ETests/Contract/ContractIdPopulation.swift b/Tests/HederaE2ETests/Contract/ContractIdPopulation.swift index 95854b94..aeb0f3df 100644 --- a/Tests/HederaE2ETests/Contract/ContractIdPopulation.swift +++ b/Tests/HederaE2ETests/Contract/ContractIdPopulation.swift @@ -22,8 +22,10 @@ import Hedera import XCTest internal final class ContractIdPopulation: XCTestCase { - internal let contractByteCode = "608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506101cb806100606000396000f3fe608060405260043610610046576000357c01000000000000000000000000000000000000000000000000000000009004806341c0e1b51461004b578063cfae321714610062575b600080fd5b34801561005757600080fd5b506100606100f2565b005b34801561006e57600080fd5b50610077610162565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100b757808201518184015260208101905061009c565b50505050905090810190601f1680156100e45780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610160573373ffffffffffffffffffffffffffffffffffffffff16ff5b565b60606040805190810160405280600d81526020017f48656c6c6f2c20776f726c64210000000000000000000000000000000000000081525090509056fea165627a7a72305820ae96fb3af7cde9c0abfe365272441894ab717f816f07f41f07b1cbede54e256e0029".data(using: .utf8)! - + internal let contractByteCode = + "608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506101cb806100606000396000f3fe608060405260043610610046576000357c01000000000000000000000000000000000000000000000000000000009004806341c0e1b51461004b578063cfae321714610062575b600080fd5b34801561005757600080fd5b506100606100f2565b005b34801561006e57600080fd5b50610077610162565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100b757808201518184015260208101905061009c565b50505050905090810190601f1680156100e45780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610160573373ffffffffffffffffffffffffffffffffffffffff16ff5b565b60606040805190810160405280600d81526020017f48656c6c6f2c20776f726c64210000000000000000000000000000000000000081525090509056fea165627a7a72305820ae96fb3af7cde9c0abfe365272441894ab717f816f07f41f07b1cbede54e256e0029" + .data(using: .utf8)! + internal func testPopulateContractIdNum() async throws { let testEnv = try TestEnvironment.nonFree @@ -45,13 +47,13 @@ internal final class ContractIdPopulation: XCTestCase { .getReceipt(testEnv.client) let contractId = try XCTUnwrap(contractCreateReceipt.contractId) + try await Task.sleep(nanoseconds: 5 * 1_000_000_000) let contractInfo = try await ContractInfoQuery(contractId: contractId).execute(testEnv.client) - let contractIdMirror = try ContractId.fromEvmAddress(0, 0, contractInfo.contractAccountId) let newContractId = try await contractIdMirror.populateContractNum(testEnv.client) - + XCTAssertEqual(contractId.num, newContractId.num) } -} \ No newline at end of file +} From 6c20f018a82372fbabbfc476f5ac42659643b54e Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Mon, 3 Jun 2024 11:22:35 -0700 Subject: [PATCH 12/18] feat: remove fatal errors Signed-off-by: Ricky Saechao --- Sources/Hedera/MirrorNodeGateway.swift | 59 ++++++------- Sources/Hedera/MirrorNodeRouter.swift | 4 +- Sources/Hedera/MirrorNodeService.swift | 110 ++++++++++++++++++------- 3 files changed, 105 insertions(+), 68 deletions(-) diff --git a/Sources/Hedera/MirrorNodeGateway.swift b/Sources/Hedera/MirrorNodeGateway.swift index 6fa0c6ef..54a6b322 100644 --- a/Sources/Hedera/MirrorNodeGateway.swift +++ b/Sources/Hedera/MirrorNodeGateway.swift @@ -49,17 +49,7 @@ internal struct MirrorNodeGateway { let responseBody = try await queryFromMirrorNode(fullApiUrl) - guard let jsonData = responseBody.data(using: .utf8) else { - throw NSError( - domain: "InvalidResponseError", code: -1, - userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) - } - - guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { - throw NSError( - domain: "InvalidResponseError", code: -1, - userInfo: [NSLocalizedDescriptionKey: "Response body is not a valid JSON object"]) - } + let jsonObject = try await deserializeJson(responseBody) return jsonObject } @@ -70,47 +60,32 @@ internal struct MirrorNodeGateway { let responseBody = try await queryFromMirrorNode(fullApiUrl) - guard let jsonData = responseBody.data(using: .utf8) else { - throw NSError( - domain: "InvalidResponseError", code: -1, - userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) - } - guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { - throw NSError( - domain: "InvalidResponseError", code: -1, - userInfo: [NSLocalizedDescriptionKey: "Response body is not a valid JSON object"]) - } + let jsonObject = try await deserializeJson(responseBody) return jsonObject } internal func getAccountTokens(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { let fullApiUrl = MirrorNodeRouter.buildApiUrl( - self.mirrorNodeUrl, MirrorNodeRouter.accountTokensRoute, idOrAliasOrEvmAddress) + self.mirrorNodeUrl, MirrorNodeRouter.tokensAccountRoute, idOrAliasOrEvmAddress) let responseBody = try await queryFromMirrorNode(fullApiUrl) - guard let jsonData = responseBody.data(using: .utf8) else { - throw NSError( - domain: "InvalidResponseError", code: -1, - userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) - } - - guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { - throw NSError( - domain: "InvalidResponseError", code: -1, - userInfo: [NSLocalizedDescriptionKey: "Response body is not a valid JSON object"]) - } + let jsonObject = try await deserializeJson(responseBody) return jsonObject } - internal func queryFromMirrorNode(_ apiUrl: String) async throws -> String { + private func queryFromMirrorNode(_ apiUrl: String) async throws -> String { let httpClient = HTTPClient(eventLoopGroupProvider: .singleton) defer { try? httpClient.syncShutdown() } + 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)) @@ -120,4 +95,20 @@ internal struct MirrorNodeGateway { return bodyString } + + func deserializeJson(_ responseBody: String) async throws -> [String: Any] { + guard let jsonData = responseBody.data(using: .utf8) else { + throw NSError( + domain: "InvalidResponseError", code: -1, + userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) + } + + guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { + throw NSError( + domain: "InvalidResponseError", code: -1, + userInfo: [NSLocalizedDescriptionKey: "Response body is not a valid JSON object"]) + } + + return jsonObject + } } diff --git a/Sources/Hedera/MirrorNodeRouter.swift b/Sources/Hedera/MirrorNodeRouter.swift index 9308f0b2..e4d71ed4 100644 --- a/Sources/Hedera/MirrorNodeRouter.swift +++ b/Sources/Hedera/MirrorNodeRouter.swift @@ -32,12 +32,12 @@ internal struct MirrorNodeRouter { public static let accountsRoute = "accounts" public static let contractsRoute = "contracts" - public static let accountTokensRoute = "account_tokens" + public static let tokensAccountRoute = "account_tokens" static let routes: [String: String] = [ accountsRoute: "/accounts/%@", contractsRoute: "/contracts/%@", - accountTokensRoute: "/accounts/%@/tokens", + tokensAccountRoute: "/accounts/%@/tokens", ] private func MirrorNodeRouter() {} diff --git a/Sources/Hedera/MirrorNodeService.swift b/Sources/Hedera/MirrorNodeService.swift index 9f0bf473..1650df3e 100644 --- a/Sources/Hedera/MirrorNodeService.swift +++ b/Sources/Hedera/MirrorNodeService.swift @@ -56,10 +56,14 @@ internal final class MirrorNodeService { } internal func getContractNum(_ evmAddress: String) async throws -> UInt64 { - let accountInfoResponse = try await self.mirrorNodeGateway.getContractInfo(evmAddress) + let contractInfoResponse = try await self.mirrorNodeGateway.getContractInfo(evmAddress) - guard let contractId = accountInfoResponse["contract_id"] else { - fatalError("Error while processing getContractNum mirror node query") + guard let contractId = contractInfoResponse["contract_id"] else { + throw NSError( + domain: "InvalidResponseError", code: -1, + userInfo: [ + NSLocalizedDescriptionKey: "Error while processing getContractNum mirror node query" + ]) } let contractIdNum = ContractId(String(describing: contractId))?.num @@ -71,12 +75,16 @@ internal final class MirrorNodeService { let accountTokensResponse = try await self.mirrorNodeGateway.getAccountTokens(evmAddress) guard let tokens = accountTokensResponse["tokens"] else { - fatalError("Error while processing getTokenBalancesForAccount mirror node query") + throw NSError( + domain: "InvalidResponseError", code: -1, + userInfo: [ + NSLocalizedDescriptionKey: "Error while processing getAccountTokens mirror node query" + ]) } var tokenBalances: [Proto_TokenBalance] = [] - guard let tokensList = tokens as? [[String: Any]] else { + guard let tokensList: [[String: Any]] = tokens as? [[String: Any]] else { throw NSError( domain: "InvalidResponseError", code: -1, userInfo: [ @@ -84,14 +92,26 @@ internal final class MirrorNodeService { ]) } tokensList.forEach { token in - let tokenId = TokenId(String(describing: token["token_id"]))?.toProtobuf() - let balance = UInt64(String(describing: token["balance"])) - let decimals = UInt32(String(describing: token["decimals"])) + var tokenId: String = "" + var balance: UInt64 = 0 + var decimals: UInt32 = 0 + + if let id = token["token_id"] as? String { + tokenId = id + } + + if let hbar = token["balance"] as? String { + balance = UInt64(hbar)! + } + + if let dec = token["decimals"] as? String { + decimals = UInt32(dec)! + } let tokenBalanceProto = Proto_TokenBalance.with { proto in - proto.tokenID = tokenId! - proto.balance = balance! - proto.decimals = decimals! + proto.tokenID = TokenId(tokenId)!.toProtobuf() + proto.balance = balance + proto.decimals = decimals } tokenBalances.append(tokenBalanceProto) @@ -114,28 +134,54 @@ internal final class MirrorNodeService { var tokenBalances: [Proto_TokenRelationship] = [] - if let tokensList = tokens as? [[String: Any]] { - try tokensList.forEach { token in - let tokenId = TokenId(String(describing: token["token_id"]))?.toProtobuf() - let balance = UInt64(String(describing: token["balance"])) - let decimals = UInt32(String(describing: token["decimals"])) - let kycStatus = String(describing: token["kyc_status"]) - let freezeStatus = String(describing: token["freeze_status"]) - let automaticAssociation = Bool(String(describing: token["automatic_assocation"])) - - let tokenRelationshipsProto = try Proto_TokenRelationship.with { proto in - proto.tokenID = tokenId! - proto.balance = balance! - proto.decimals = decimals! - proto.kycStatus = try getTokenKycStatusFromString(kycStatus) - proto.freezeStatus = try getTokenFreezeStatusFromString(freezeStatus) - proto.automaticAssociation = automaticAssociation! - } - - tokenBalances.append(tokenRelationshipsProto) + guard let tokensList: [[String: Any]] = tokens as? [[String: Any]] else { + throw NSError( + domain: "InvalidResponseError", code: -1, + userInfo: [ + NSLocalizedDescriptionKey: "Error while processing getTokenBalancesForAccount mirror node query" + ]) + } + try tokensList.forEach { token in + var tokenId: String = "" + var balance: UInt64 = 0 + var decimals: UInt32 = 0 + var kycStatus: String = "" + var freezeStatus: String = "" + var automaticAssociation: Bool = false + if let id = token["token_id"] as? String { + tokenId = id + } + + if let hbar = token["balance"] as? String { + balance = UInt64(hbar)! + } + + if let dec = token["decimals"] as? String { + decimals = UInt32(dec)! + } + + if let kyc = token["kyc_status"] as? String { + kycStatus = kyc + } + + if let freeze = token["freeze_status"] as? String { + freezeStatus = freeze + } + + if let auto = token["automatic_association"] as? String { + automaticAssociation = Bool(auto)! + } + + let tokenRelationshipsProto = try Proto_TokenRelationship.with { proto in + proto.tokenID = TokenId(tokenId)!.toProtobuf() + proto.balance = balance + proto.decimals = decimals + proto.kycStatus = try getTokenKycStatusFromString(kycStatus) + proto.freezeStatus = try getTokenFreezeStatusFromString(freezeStatus) + proto.automaticAssociation = automaticAssociation } - } else { - fatalError("Error while processing getTokenRelationshipsForAccount mirror node query") + + tokenBalances.append(tokenRelationshipsProto) } return tokenBalances From 1637babb30a942657c96c15e894eb5d2b42e4eab Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Mon, 3 Jun 2024 11:30:25 -0700 Subject: [PATCH 13/18] fix: TokenBalance and TokenRelationship query response mapping Signed-off-by: Ricky Saechao --- Sources/Hedera/Account/AccountBalance.swift | 12 +++++- .../Hedera/Account/AccountBalanceQuery.swift | 16 ++++++- Sources/Hedera/Account/AccountId.swift | 9 ++++ Sources/Hedera/Account/AccountInfoQuery.swift | 43 +++++++++---------- Sources/Hedera/Contract/ContractInfo.swift | 2 +- .../Hedera/Contract/ContractInfoQuery.swift | 35 +++++++-------- .../Contract/ContractInfo.swift | 2 +- 7 files changed, 76 insertions(+), 43 deletions(-) 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 d05eb1d2..c92e778f 100644 --- a/Sources/Hedera/Account/AccountBalanceQuery.swift +++ b/Sources/Hedera/Account/AccountBalanceQuery.swift @@ -88,11 +88,25 @@ public final class AccountBalanceQuery: Query { 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(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `cryptogetAccountBalance`") } - return try .fromProtobuf(proto) + let accountId = try AccountId.fromProtobuf(proto.accountID) + let tokenBalanceProto = try await mirrorNodeService.getTokenBalancesForAccount(String(accountId.num)) + + // var tokenBalances: [TokenId: UInt64] = [:] + + // for balance in tokenBalanceProto { + // tokenBalances[.fromProtobuf(balance.tokenID)] = balance.balance + // } + + return AccountBalance( + accountId: accountId, hbars: .fromTinybars(Int64(proto.balance)), + tokensInner: .fromProtobuf(tokenBalanceProto)) } public override func validateChecksums(on ledgerId: LedgerId) throws { diff --git a/Sources/Hedera/Account/AccountId.swift b/Sources/Hedera/Account/AccountId.swift index 1bcedc83..6b19e469 100644 --- a/Sources/Hedera/Account/AccountId.swift +++ b/Sources/Hedera/Account/AccountId.swift @@ -135,6 +135,15 @@ public struct AccountId: Sendable, EntityId, ValidateChecksums { public func toBytes() -> Data { toProtobufBytes() } + + public func populateAccountIdNum(_ client: Client) async throws -> Self { + let mirrorNodeGateway = try MirrorNodeGateway.forClient(client) + let mirrorNodeService = MirrorNodeService(mirrorNodeGateway) + + let accountNumFromMirror = try await mirrorNodeService.getAccountNum(self.evmAddress!.toString()) + + return Self(shard: shard, realm: realm, num: accountNumFromMirror) + } } extension AccountId: TryProtobufCodable { diff --git a/Sources/Hedera/Account/AccountInfoQuery.swift b/Sources/Hedera/Account/AccountInfoQuery.swift index 7bce0179..82f53f37 100644 --- a/Sources/Hedera/Account/AccountInfoQuery.swift +++ b/Sources/Hedera/Account/AccountInfoQuery.swift @@ -69,10 +69,9 @@ public final class AccountInfoQuery: Query { throw HError.fromProtobuf("unexpected \(response) received, expected `cryptoGetInfo`") } - let accountInfoProto = proto.accountInfo - let accountId = try AccountId.fromProtobuf(accountInfoProto.accountID) + let accountInfo = try AccountInfo.fromProtobuf(proto.accountInfo) let tokenRelationshipsProto = try await mirrorNodeService.getTokenRelationshipsForAccount( - String(describing: accountId.num)) + String(describing: accountInfo.accountId.num)) var tokenRelationships: [TokenId: TokenRelationship] = [:] @@ -81,26 +80,26 @@ public final class AccountInfoQuery: Query { } return AccountInfo( - accountId: try AccountId.fromProtobuf(accountInfoProto.accountID), - contractAccountId: accountInfoProto.contractAccountID, - isDeleted: accountInfoProto.deleted, - proxyAccountId: try .fromProtobuf(accountInfoProto.proxyAccountID), - proxyReceived: Hbar.fromTinybars(accountInfoProto.proxyReceived), - key: try .fromProtobuf(accountInfoProto.key), - balance: .fromTinybars(Int64(accountInfoProto.balance)), - sendRecordThreshold: Hbar.fromTinybars(Int64(accountInfoProto.generateSendRecordThreshold)), - receiveRecordThreshold: Hbar.fromTinybars(Int64(accountInfoProto.generateReceiveRecordThreshold)), - isReceiverSignatureRequired: accountInfoProto.receiverSigRequired, - expirationTime: .fromProtobuf(accountInfoProto.expirationTime), - autoRenewPeriod: .fromProtobuf(accountInfoProto.autoRenewPeriod), - accountMemo: accountInfoProto.memo, - ownedNfts: UInt64(accountInfoProto.ownedNfts), - maxAutomaticTokenAssociations: UInt32(accountInfoProto.maxAutomaticTokenAssociations), - aliasKey: try .fromAliasBytes(accountInfoProto.alias), - ethereumNonce: UInt64(accountInfoProto.ethereumNonce), + accountId: accountInfo.accountId, + contractAccountId: accountInfo.contractAccountId, + isDeleted: accountInfo.isDeleted, + proxyAccountId: accountInfo.proxyAccountId, + proxyReceived: accountInfo.proxyReceived, + key: accountInfo.key, + balance: accountInfo.balance, + sendRecordThreshold: accountInfo.sendRecordThreshold, + receiveRecordThreshold: accountInfo.receiveRecordThreshold, + isReceiverSignatureRequired: accountInfo.isReceiverSignatureRequired, + expirationTime: accountInfo.expirationTime, + autoRenewPeriod: accountInfo.autoRenewPeriod, + accountMemo: accountInfo.accountMemo, + ownedNfts: accountInfo.ownedNfts, + maxAutomaticTokenAssociations: accountInfo.maxAutomaticTokenAssociations, + aliasKey: accountInfo.aliasKey, + ethereumNonce: accountInfo.ethereumNonce, tokenRelationships: tokenRelationships, - ledgerId: .fromBytes(accountInfoProto.ledgerID), - staking: try .fromProtobuf(accountInfoProto.stakingInfo) + ledgerId: accountInfo.ledgerId, + staking: accountInfo.staking ) } diff --git a/Sources/Hedera/Contract/ContractInfo.swift b/Sources/Hedera/Contract/ContractInfo.swift index 6d7275eb..f84ae36b 100644 --- a/Sources/Hedera/Contract/ContractInfo.swift +++ b/Sources/Hedera/Contract/ContractInfo.swift @@ -104,7 +104,7 @@ extension ContractInfo: TryProtobufCodable { self.init( contractId: try .fromProtobuf(proto.contractID), - accountId: try .fromProtobuf(proto.accountID), + accountId: try AccountId.fromProtobuf(proto.accountID), contractAccountId: proto.contractAccountID, adminKey: try .fromProtobuf(adminKey), expirationTime: .fromProtobuf(expirationTime), diff --git a/Sources/Hedera/Contract/ContractInfoQuery.swift b/Sources/Hedera/Contract/ContractInfoQuery.swift index 6e6a1d17..08a095cb 100644 --- a/Sources/Hedera/Contract/ContractInfoQuery.swift +++ b/Sources/Hedera/Contract/ContractInfoQuery.swift @@ -65,10 +65,10 @@ public final class ContractInfoQuery: Query { guard case .contractGetInfo(let proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `contractGetInfo`") } - let contractInfoProto = proto.contractInfo - let contractId = try ContractId.fromProtobuf(contractInfoProto.contractID) + + let contractInfo = try ContractInfo.fromProtobuf(proto.contractInfo) let tokenRelationshipsProto = try await mirrorNodeService.getTokenRelationshipsForAccount( - String(describing: contractId.num)) + String(describing: contractInfo.contractId.num)) var tokenRelationships: [TokenId: TokenRelationship] = [:] @@ -77,21 +77,22 @@ public final class ContractInfoQuery: Query { } return ContractInfo( - contractId: try ContractId.fromProtobuf(contractInfoProto.contractID), - accountId: try AccountId.fromProtobuf(contractInfoProto.accountID), - contractAccountId: contractInfoProto.contractAccountID, - adminKey: try .fromProtobuf(contractInfoProto.adminKey), - expirationTime: .fromProtobuf(contractInfoProto.expirationTime), - autoRenewPeriod: .fromProtobuf(contractInfoProto.autoRenewPeriod), - storage: UInt64(contractInfoProto.storage), - contractMemo: contractInfoProto.memo, - balance: .fromTinybars(Int64(contractInfoProto.balance)), - isDeleted: contractInfoProto.deleted, - autoRenewAccountId: try .fromProtobuf(contractInfoProto.autoRenewAccountID), - maxAutomaticTokenAssociations: UInt32(contractInfoProto.maxAutomaticTokenAssociations), + contractId: contractInfo.contractId, + accountId: contractInfo.accountId, + contractAccountId: contractInfo.contractAccountId, + adminKey: contractInfo.adminKey, + expirationTime: contractInfo.expirationTime, + autoRenewPeriod: contractInfo.autoRenewPeriod, + storage: contractInfo.storage, + contractMemo: contractInfo.contractMemo, + balance: contractInfo.balance, + isDeleted: contractInfo.isDeleted, + autoRenewAccountId: contractInfo.autoRenewAccountId, + maxAutomaticTokenAssociations: contractInfo.maxAutomaticTokenAssociations, tokenRelationships: tokenRelationships, - ledgerId: .fromBytes(contractInfoProto.ledgerID), - stakingInfo: try .fromProtobuf(contractInfoProto.stakingInfo)) + ledgerId: contractInfo.ledgerId, + stakingInfo: contractInfo.stakingInfo + ) } internal override func validateChecksums(on ledgerId: LedgerId) throws { diff --git a/Tests/HederaE2ETests/Contract/ContractInfo.swift b/Tests/HederaE2ETests/Contract/ContractInfo.swift index 9093cd68..6f37b56c 100644 --- a/Tests/HederaE2ETests/Contract/ContractInfo.swift +++ b/Tests/HederaE2ETests/Contract/ContractInfo.swift @@ -22,7 +22,7 @@ import Hedera import XCTest internal final class ContractInfo: XCTestCase { - internal func testQueryBoo() async throws { + internal func testQuery() async throws { let testEnv = try TestEnvironment.nonFree let contractId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) From 8944b558ef49c843474c14c533cd07eca81e42de Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Mon, 3 Jun 2024 11:34:30 -0700 Subject: [PATCH 14/18] test: uncomment account balance e2e test Signed-off-by: Ricky Saechao --- .../Hedera/Account/AccountBalanceQuery.swift | 6 -- .../Account/AccountBalance.swift | 86 +++++++++---------- 2 files changed, 42 insertions(+), 50 deletions(-) diff --git a/Sources/Hedera/Account/AccountBalanceQuery.swift b/Sources/Hedera/Account/AccountBalanceQuery.swift index c92e778f..e977e7f0 100644 --- a/Sources/Hedera/Account/AccountBalanceQuery.swift +++ b/Sources/Hedera/Account/AccountBalanceQuery.swift @@ -98,12 +98,6 @@ public final class AccountBalanceQuery: Query { let accountId = try AccountId.fromProtobuf(proto.accountID) let tokenBalanceProto = try await mirrorNodeService.getTokenBalancesForAccount(String(accountId.num)) - // var tokenBalances: [TokenId: UInt64] = [:] - - // for balance in tokenBalanceProto { - // tokenBalances[.fromProtobuf(balance.tokenID)] = balance.balance - // } - return AccountBalance( accountId: accountId, hbars: .fromTinybars(Int64(proto.balance)), tokensInner: .fromProtobuf(tokenBalanceProto)) diff --git a/Tests/HederaE2ETests/Account/AccountBalance.swift b/Tests/HederaE2ETests/Account/AccountBalance.swift index 14906cad..fb469c7a 100644 --- a/Tests/HederaE2ETests/Account/AccountBalance.swift +++ b/Tests/HederaE2ETests/Account/AccountBalance.swift @@ -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) + } } From 10fb994ab28a88218c931f5ecb41ab0d3552e41e Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Mon, 3 Jun 2024 11:35:37 -0700 Subject: [PATCH 15/18] chore: update test snapshots and pre check status Signed-off-by: Ricky Saechao --- Sources/Hedera/MirrorNodeRouter.swift | 2 +- Tests/HederaE2ETests/Contract/ContractCreate.swift | 7 +++---- .../__Snapshots__/ContractInfoTests/testFromBytes.1.txt | 2 +- .../__Snapshots__/ContractInfoTests/testFromProtobuf.1.txt | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Sources/Hedera/MirrorNodeRouter.swift b/Sources/Hedera/MirrorNodeRouter.swift index e4d71ed4..841cf262 100644 --- a/Sources/Hedera/MirrorNodeRouter.swift +++ b/Sources/Hedera/MirrorNodeRouter.swift @@ -40,7 +40,7 @@ internal struct MirrorNodeRouter { tokensAccountRoute: "/accounts/%@/tokens", ] - private func MirrorNodeRouter() {} + private func mirrorNodeRouter() {} static func getMirrorNodeUrl(_ mirrorNetwork: [String], _ ledgerId: LedgerId?) throws -> String { var mirrorNodeAddress: String = "" 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 From 7a424d85fad642ee7bce9c7da4850c353ae69924 Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Wed, 5 Jun 2024 14:19:56 -0700 Subject: [PATCH 16/18] fix: requested changes Signed-off-by: Ricky Saechao --- .../Hedera/Account/AccountBalanceQuery.swift | 12 +- Sources/Hedera/Account/AccountId.swift | 9 -- Sources/Hedera/Account/AccountInfo.swift | 2 +- Sources/Hedera/Account/AccountInfoQuery.swift | 34 +--- Sources/Hedera/ChunkedTransaction.swift | 10 +- Sources/Hedera/Contract/ContractId.swift | 11 -- Sources/Hedera/Contract/ContractInfo.swift | 4 +- .../Hedera/Contract/ContractInfoQuery.swift | 30 +--- Sources/Hedera/Execute.swift | 17 +- Sources/Hedera/HError.swift | 5 + Sources/Hedera/MirrorNodeGateway.swift | 15 +- Sources/Hedera/MirrorNodeRouter.swift | 20 +-- Sources/Hedera/MirrorNodeService.swift | 147 +++++------------- Sources/Hedera/PingQuery.swift | 5 +- Sources/Hedera/Query.swift | 12 +- Sources/Hedera/QueryCost.swift | 5 +- Sources/Hedera/Token/TokenRelationship.swift | 54 +++---- Sources/Hedera/Transaction.swift | 5 +- .../Transaction/TransactionSources.swift | 5 +- .../Account/AccountBalance.swift | 2 +- .../Contract/ContractIdPopulation.swift | 59 ------- 21 files changed, 143 insertions(+), 320 deletions(-) delete mode 100644 Tests/HederaE2ETests/Contract/ContractIdPopulation.swift diff --git a/Sources/Hedera/Account/AccountBalanceQuery.swift b/Sources/Hedera/Account/AccountBalanceQuery.swift index e977e7f0..3de350d0 100644 --- a/Sources/Hedera/Account/AccountBalanceQuery.swift +++ b/Sources/Hedera/Account/AccountBalanceQuery.swift @@ -91,16 +91,16 @@ public final class AccountBalanceQuery: Query { let mirrorNodeGateway = try MirrorNodeGateway.forNetwork(context.mirrorNetworkNodes, context.ledgerId) let mirrorNodeService = MirrorNodeService.init(mirrorNodeGateway) - guard case .cryptogetAccountBalance(let proto) = response else { + guard case .cryptogetAccountBalance(var proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `cryptogetAccountBalance`") } - let accountId = try AccountId.fromProtobuf(proto.accountID) - let tokenBalanceProto = try await mirrorNodeService.getTokenBalancesForAccount(String(accountId.num)) + let tokenBalanceProto = try await mirrorNodeService.getTokenBalancesForAccount( + String(proto.accountID.accountNum)) - return AccountBalance( - accountId: accountId, hbars: .fromTinybars(Int64(proto.balance)), - tokensInner: .fromProtobuf(tokenBalanceProto)) + proto.tokenBalances = tokenBalanceProto + + return try .fromProtobuf(proto) } public override func validateChecksums(on ledgerId: LedgerId) throws { diff --git a/Sources/Hedera/Account/AccountId.swift b/Sources/Hedera/Account/AccountId.swift index 6b19e469..1bcedc83 100644 --- a/Sources/Hedera/Account/AccountId.swift +++ b/Sources/Hedera/Account/AccountId.swift @@ -135,15 +135,6 @@ public struct AccountId: Sendable, EntityId, ValidateChecksums { public func toBytes() -> Data { toProtobufBytes() } - - public func populateAccountIdNum(_ client: Client) async throws -> Self { - let mirrorNodeGateway = try MirrorNodeGateway.forClient(client) - let mirrorNodeService = MirrorNodeService(mirrorNodeGateway) - - let accountNumFromMirror = try await mirrorNodeService.getAccountNum(self.evmAddress!.toString()) - - return Self(shard: shard, realm: realm, num: accountNumFromMirror) - } } extension AccountId: TryProtobufCodable { diff --git a/Sources/Hedera/Account/AccountInfo.swift b/Sources/Hedera/Account/AccountInfo.swift index 18e99e19..c49a1f11 100644 --- a/Sources/Hedera/Account/AccountInfo.swift +++ b/Sources/Hedera/Account/AccountInfo.swift @@ -156,7 +156,7 @@ public struct AccountInfo: Sendable { /// Staking metadata for this account. public let staking: StakingInfo? - /// Staking metadata for this account. + /// Token relationships for this account. public let tokenRelationships: [TokenId: TokenRelationship] /// Decode `Self` from protobuf-encoded `bytes`. diff --git a/Sources/Hedera/Account/AccountInfoQuery.swift b/Sources/Hedera/Account/AccountInfoQuery.swift index 82f53f37..0670f84a 100644 --- a/Sources/Hedera/Account/AccountInfoQuery.swift +++ b/Sources/Hedera/Account/AccountInfoQuery.swift @@ -65,42 +65,16 @@ public final class AccountInfoQuery: Query { let mirrorNodeGateway = try MirrorNodeGateway.forNetwork(context.mirrorNetworkNodes, context.ledgerId) let mirrorNodeService = MirrorNodeService.init(mirrorNodeGateway) - guard case .cryptoGetInfo(let proto) = response else { + guard case .cryptoGetInfo(var proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `cryptoGetInfo`") } - let accountInfo = try AccountInfo.fromProtobuf(proto.accountInfo) let tokenRelationshipsProto = try await mirrorNodeService.getTokenRelationshipsForAccount( - String(describing: accountInfo.accountId.num)) + String(describing: try AccountId.fromProtobuf(proto.accountInfo.accountID).num)) - var tokenRelationships: [TokenId: TokenRelationship] = [:] + proto.accountInfo.tokenRelationships = tokenRelationshipsProto - for relationship in tokenRelationshipsProto { - tokenRelationships[.fromProtobuf(relationship.tokenID)] = try TokenRelationship.fromProtobuf(relationship) - } - - return AccountInfo( - accountId: accountInfo.accountId, - contractAccountId: accountInfo.contractAccountId, - isDeleted: accountInfo.isDeleted, - proxyAccountId: accountInfo.proxyAccountId, - proxyReceived: accountInfo.proxyReceived, - key: accountInfo.key, - balance: accountInfo.balance, - sendRecordThreshold: accountInfo.sendRecordThreshold, - receiveRecordThreshold: accountInfo.receiveRecordThreshold, - isReceiverSignatureRequired: accountInfo.isReceiverSignatureRequired, - expirationTime: accountInfo.expirationTime, - autoRenewPeriod: accountInfo.autoRenewPeriod, - accountMemo: accountInfo.accountMemo, - ownedNfts: accountInfo.ownedNfts, - maxAutomaticTokenAssociations: accountInfo.maxAutomaticTokenAssociations, - aliasKey: accountInfo.aliasKey, - ethereumNonce: accountInfo.ethereumNonce, - tokenRelationships: tokenRelationships, - ledgerId: accountInfo.ledgerId, - staking: accountInfo.staking - ) + return try .fromProtobuf(proto.accountInfo) } internal override func validateChecksums(on ledgerId: LedgerId) throws { diff --git a/Sources/Hedera/ChunkedTransaction.swift b/Sources/Hedera/ChunkedTransaction.swift index 10b7789b..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(_ client: Client, _ 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(_ client: Client, _ 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/ContractId.swift b/Sources/Hedera/Contract/ContractId.swift index c0cdadec..e1d83d99 100644 --- a/Sources/Hedera/Contract/ContractId.swift +++ b/Sources/Hedera/Contract/ContractId.swift @@ -119,17 +119,6 @@ public struct ContractId: EntityId { public func toBytes() -> Data { toProtobufBytes() } - - public func populateContractNum(_ client: Client) async throws -> Self { - let address = try EvmAddress.fromBytes(self.evmAddress!) - - let mirrorNodeGateway = try MirrorNodeGateway.forClient(client) - let mirrorNodeService = MirrorNodeService(mirrorNodeGateway) - - let contractNum = try await mirrorNodeService.getContractNum(address.toString()) - - return Self(shard: shard, realm: realm, num: contractNum) - } } #if compiler(>=5.7) diff --git a/Sources/Hedera/Contract/ContractInfo.swift b/Sources/Hedera/Contract/ContractInfo.swift index f84ae36b..a38ce300 100644 --- a/Sources/Hedera/Contract/ContractInfo.swift +++ b/Sources/Hedera/Contract/ContractInfo.swift @@ -63,7 +63,7 @@ public struct ContractInfo { /// The tokens associated to the contract /// - /// Query mirror node + /// Note: Query mirror node for token relationships. public let tokenRelationships: [TokenId: TokenRelationship] /// Ledger ID for the network the response was returned from. @@ -104,7 +104,7 @@ extension ContractInfo: TryProtobufCodable { self.init( contractId: try .fromProtobuf(proto.contractID), - accountId: try AccountId.fromProtobuf(proto.accountID), + accountId: try .fromProtobuf(proto.accountID), contractAccountId: proto.contractAccountID, adminKey: try .fromProtobuf(adminKey), expirationTime: .fromProtobuf(expirationTime), diff --git a/Sources/Hedera/Contract/ContractInfoQuery.swift b/Sources/Hedera/Contract/ContractInfoQuery.swift index 08a095cb..14e409de 100644 --- a/Sources/Hedera/Contract/ContractInfoQuery.swift +++ b/Sources/Hedera/Contract/ContractInfoQuery.swift @@ -42,7 +42,6 @@ public final class ContractInfoQuery: Query { } internal override func toQueryProtobufWith(_ header: Proto_QueryHeader) -> Proto_Query { - .with { proto in proto.contractGetInfo = .with { proto in proto.header = header @@ -62,37 +61,16 @@ public final class ContractInfoQuery: Query { let mirrorNodeGateway = try MirrorNodeGateway.forNetwork(context.mirrorNetworkNodes, context.ledgerId) let mirrorNodeService = MirrorNodeService.init(mirrorNodeGateway) - guard case .contractGetInfo(let proto) = response else { + guard case .contractGetInfo(var proto) = response else { throw HError.fromProtobuf("unexpected \(response) received, expected `contractGetInfo`") } - let contractInfo = try ContractInfo.fromProtobuf(proto.contractInfo) let tokenRelationshipsProto = try await mirrorNodeService.getTokenRelationshipsForAccount( - String(describing: contractInfo.contractId.num)) - - var tokenRelationships: [TokenId: TokenRelationship] = [:] + String(describing: proto.contractInfo.accountID.accountNum)) - for relationship in tokenRelationshipsProto { - tokenRelationships[.fromProtobuf(relationship.tokenID)] = try TokenRelationship.fromProtobuf(relationship) - } + proto.contractInfo.tokenRelationships = tokenRelationshipsProto - return ContractInfo( - contractId: contractInfo.contractId, - accountId: contractInfo.accountId, - contractAccountId: contractInfo.contractAccountId, - adminKey: contractInfo.adminKey, - expirationTime: contractInfo.expirationTime, - autoRenewPeriod: contractInfo.autoRenewPeriod, - storage: contractInfo.storage, - contractMemo: contractInfo.contractMemo, - balance: contractInfo.balance, - isDeleted: contractInfo.isDeleted, - autoRenewAccountId: contractInfo.autoRenewAccountId, - maxAutomaticTokenAssociations: contractInfo.maxAutomaticTokenAssociations, - tokenRelationships: tokenRelationships, - ledgerId: contractInfo.ledgerId, - stakingInfo: contractInfo.stakingInfo - ) + return try ContractInfo.fromProtobuf(proto.contractInfo) } internal override func validateChecksums(on ledgerId: LedgerId) throws { diff --git a/Sources/Hedera/Execute.swift b/Sources/Hedera/Execute.swift index 21551896..ed2aebe5 100644 --- a/Sources/Hedera/Execute.swift +++ b/Sources/Hedera/Execute.swift @@ -60,7 +60,10 @@ 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(_ client: Client, _ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> ( + func makeRequest( + _ ledgerId: LedgerId?, _ mirrorNodeNetworks: [String], _ transactionId: TransactionId?, + _ nodeAccountId: AccountId + ) throws -> ( GrpcRequest, Context ) @@ -97,7 +100,8 @@ private struct ExecuteContext { fileprivate let network: Network fileprivate let backoffConfig: LegacyExponentialBackoff fileprivate let maxAttempts: Int - fileprivate let client: Client + fileprivate let ledgerId: LedgerId? + fileprivate let mirrorNodeNetworks: [String] // timeout for a single grpc request. fileprivate let grpcTimeout: Duration? } @@ -153,7 +157,8 @@ internal func executeAny( network: client.net, backoffConfig: backoffBuilder, maxAttempts: backoff.maxAttempts, - client: client, + ledgerId: client.ledgerId, + mirrorNodeNetworks: client.mirrorNetwork, grpcTimeout: nil ), executable: executable) @@ -189,7 +194,8 @@ private func executeAnyInner( } let (nodeAccountId, channel) = ctx.network.channel(for: nodeIndex) - let (request, context) = try executable.makeRequest(ctx.client, transactionId, nodeAccountId) + let (request, context) = try executable.makeRequest( + ctx.ledgerId, ctx.mirrorNodeNetworks, transactionId, nodeAccountId) let response: E.GrpcResponse do { @@ -313,7 +319,8 @@ private struct NodeIndexesGeneratorMap: AsyncSequence, AsyncIteratorProtocol { network: ctx.network, backoffConfig: ctx.backoffConfig, maxAttempts: ctx.maxAttempts, - client: ctx.client, + ledgerId: ctx.ledgerId, + mirrorNodeNetworks: ctx.mirrorNodeNetworks, grpcTimeout: ctx.grpcTimeout ), executable: request 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 index 54a6b322..8a555bb5 100644 --- a/Sources/Hedera/MirrorNodeGateway.swift +++ b/Sources/Hedera/MirrorNodeGateway.swift @@ -45,7 +45,7 @@ internal struct MirrorNodeGateway { internal func getAccountInfo(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { let fullApiUrl = MirrorNodeRouter.buildApiUrl( - self.mirrorNodeUrl, MirrorNodeRouter.accountsRoute, idOrAliasOrEvmAddress) + self.mirrorNodeUrl, MirrorNodeRouter.MirrorNodeRoute.accountInfoRoute, idOrAliasOrEvmAddress) let responseBody = try await queryFromMirrorNode(fullApiUrl) @@ -56,7 +56,7 @@ internal struct MirrorNodeGateway { internal func getContractInfo(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { let fullApiUrl = MirrorNodeRouter.buildApiUrl( - self.mirrorNodeUrl, MirrorNodeRouter.contractsRoute, idOrAliasOrEvmAddress) + self.mirrorNodeUrl, MirrorNodeRouter.MirrorNodeRoute.contractInfoRoute, idOrAliasOrEvmAddress) let responseBody = try await queryFromMirrorNode(fullApiUrl) @@ -67,7 +67,7 @@ internal struct MirrorNodeGateway { internal func getAccountTokens(_ idOrAliasOrEvmAddress: String) async throws -> [String: Any] { let fullApiUrl = MirrorNodeRouter.buildApiUrl( - self.mirrorNodeUrl, MirrorNodeRouter.tokensAccountRoute, idOrAliasOrEvmAddress) + self.mirrorNodeUrl, MirrorNodeRouter.MirrorNodeRoute.tokenRelationshipsRoute, idOrAliasOrEvmAddress) let responseBody = try await queryFromMirrorNode(fullApiUrl) @@ -82,6 +82,7 @@ internal struct MirrorNodeGateway { try? httpClient.syncShutdown() } + // 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) } @@ -98,15 +99,11 @@ internal struct MirrorNodeGateway { func deserializeJson(_ responseBody: String) async throws -> [String: Any] { guard let jsonData = responseBody.data(using: .utf8) else { - throw NSError( - domain: "InvalidResponseError", code: -1, - userInfo: [NSLocalizedDescriptionKey: "Response body is not valid UTF-8"]) + throw HError.mirrorNodeQuery("Response body is not valid UTF-8") } guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { - throw NSError( - domain: "InvalidResponseError", code: -1, - userInfo: [NSLocalizedDescriptionKey: "Response body is not a valid JSON object"]) + 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 index 841cf262..e55eb702 100644 --- a/Sources/Hedera/MirrorNodeRouter.swift +++ b/Sources/Hedera/MirrorNodeRouter.swift @@ -30,17 +30,11 @@ internal struct MirrorNodeRouter { static let localNodePort = "5551" - public static let accountsRoute = "accounts" - public static let contractsRoute = "contracts" - public static let tokensAccountRoute = "account_tokens" - - static let routes: [String: String] = [ - accountsRoute: "/accounts/%@", - contractsRoute: "/contracts/%@", - tokensAccountRoute: "/accounts/%@/tokens", - ] - - private func mirrorNodeRouter() {} + 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 = "" @@ -62,7 +56,7 @@ internal struct MirrorNodeRouter { return fullMirrorNodeUrl } - static func buildApiUrl(_ mirrorNodeUrl: String, _ route: String, _ id: String) -> String { - return String("\(mirrorNodeUrl)\(apiVersion)\(String(format: "\(String(describing: routes[route]!))", id))") + 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 index 1650df3e..83bb7a19 100644 --- a/Sources/Hedera/MirrorNodeService.swift +++ b/Sources/Hedera/MirrorNodeService.swift @@ -29,162 +29,87 @@ internal final class MirrorNodeService { self.mirrorNodeGateway = mirrorNodeGateway } - internal func getAccountNum(_ evmAddress: String) async throws -> UInt64 { - let accountInfoResponse = try await self.mirrorNodeGateway.getAccountInfo(evmAddress) - - guard let accountId = accountInfoResponse["account"] else { - throw NSError( - domain: "InvalidResponseError", code: -1, - userInfo: [NSLocalizedDescriptionKey: "Error while processing getAccountInfo mirror node query"]) - } - - let accountNum = AccountId(String(describing: accountId))?.num - - return accountNum! - } - - internal func getAccountEvmAddress(_ num: UInt64) async throws -> EvmAddress { - let accountInfoResponse = try await self.mirrorNodeGateway.getAccountInfo(String(describing: num)) - - guard let addressAny = accountInfoResponse["evm_address"] else { - fatalError("Error while processing getAccountEvmAddress mirror node query") - } - - let evmAddress = AccountId(String(describing: addressAny))?.evmAddress - - return evmAddress! - } - - internal func getContractNum(_ evmAddress: String) async throws -> UInt64 { - let contractInfoResponse = try await self.mirrorNodeGateway.getContractInfo(evmAddress) - - guard let contractId = contractInfoResponse["contract_id"] else { - throw NSError( - domain: "InvalidResponseError", code: -1, - userInfo: [ - NSLocalizedDescriptionKey: "Error while processing getContractNum mirror node query" - ]) - } - - let contractIdNum = ContractId(String(describing: contractId))?.num - - return contractIdNum! - } - - internal func getTokenBalancesForAccount(_ evmAddress: String) async throws -> [Proto_TokenBalance] { - let accountTokensResponse = try await self.mirrorNodeGateway.getAccountTokens(evmAddress) + internal func getTokenBalancesForAccount(_ idNumOrEvmAddress: String) async throws -> [Proto_TokenBalance] { + let accountTokensResponse = try await self.mirrorNodeGateway.getAccountTokens(idNumOrEvmAddress) guard let tokens = accountTokensResponse["tokens"] else { - throw NSError( - domain: "InvalidResponseError", code: -1, - userInfo: [ - NSLocalizedDescriptionKey: "Error while processing getAccountTokens mirror node query" - ]) + throw HError.mirrorNodeQuery("Error in fetching token relationships for account") } - var tokenBalances: [Proto_TokenBalance] = [] - guard let tokensList: [[String: Any]] = tokens as? [[String: Any]] else { - throw NSError( - domain: "InvalidResponseError", code: -1, - userInfo: [ - NSLocalizedDescriptionKey: "Error while processing getTokenBalancesForAccount mirror node query" - ]) + throw HError.mirrorNodeQuery("Error in converting tokens to array") } - tokensList.forEach { token in - var tokenId: String = "" - var balance: UInt64 = 0 - var decimals: UInt32 = 0 - if let id = token["token_id"] as? String { - tokenId = id + 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") } - if let hbar = token["balance"] as? String { - balance = UInt64(hbar)! + guard let balance = token["balance"] as? UInt64 else { + throw HError.mirrorNodeQuery("Error while converting `balance` to unsigned int") } - if let dec = token["decimals"] as? String { - decimals = UInt32(dec)! + guard let decimals = token["decimals"] as? UInt32 else { + throw HError.mirrorNodeQuery("Error while converting `decimals` to unsigned int") } - let tokenBalanceProto = Proto_TokenBalance.with { proto in - proto.tokenID = TokenId(tokenId)!.toProtobuf() + return Proto_TokenBalance.with { proto in + proto.tokenID = tokenId.toProtobuf() proto.balance = balance proto.decimals = decimals } - - tokenBalances.append(tokenBalanceProto) } return tokenBalances } - internal func getTokenRelationshipsForAccount(_ evmAddress: String) async throws -> [Proto_TokenRelationship] { - let accountTokensResponse = try await self.mirrorNodeGateway.getAccountTokens(evmAddress) + internal func getTokenRelationshipsForAccount(_ idNumOrEvmAddress: String) async throws -> [Proto_TokenRelationship] { + let accountTokensResponse = try await self.mirrorNodeGateway.getAccountTokens(idNumOrEvmAddress) guard let tokens = accountTokensResponse["tokens"] else { - throw NSError( - domain: "InvalidResponseError", code: -1, - userInfo: [ - NSLocalizedDescriptionKey: - "Error while processing getTokenRelationshipsForAccount mirror node query" - ]) + throw HError.mirrorNodeQuery("Error in fetching token relationships for account") } - var tokenBalances: [Proto_TokenRelationship] = [] - guard let tokensList: [[String: Any]] = tokens as? [[String: Any]] else { - throw NSError( - domain: "InvalidResponseError", code: -1, - userInfo: [ - NSLocalizedDescriptionKey: "Error while processing getTokenBalancesForAccount mirror node query" - ]) + throw HError.mirrorNodeQuery("Error in converting tokens to array") } - try tokensList.forEach { token in - var tokenId: String = "" - var balance: UInt64 = 0 - var decimals: UInt32 = 0 - var kycStatus: String = "" - var freezeStatus: String = "" - var automaticAssociation: Bool = false - if let id = token["token_id"] as? String { - tokenId = id + + 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") } - if let hbar = token["balance"] as? String { - balance = UInt64(hbar)! + guard let balance = token["balance"] as? UInt64 else { + throw HError.mirrorNodeQuery("Error while converting `balance` to unsigned int") } - if let dec = token["decimals"] as? String { - decimals = UInt32(dec)! + guard let decimals = token["decimals"] as? UInt32 else { + throw HError.mirrorNodeQuery("Error while converting `decimals` to unsigned int") } - if let kyc = token["kyc_status"] as? String { - kycStatus = kyc + guard let kycStatus = token["kyc_status"] as? String else { + throw HError.mirrorNodeQuery("Error while converting `kyc status` to string") } - if let freeze = token["freeze_status"] as? String { - freezeStatus = freeze + guard let freezeStatus = token["freeze_status"] as? String else { + throw HError.mirrorNodeQuery("Error while processing freeze status as string") } - if let auto = token["automatic_association"] as? String { - automaticAssociation = Bool(auto)! + guard let automaticAssociation = token["automatic_association"] as? Bool else { + throw HError.mirrorNodeQuery("Error while processing automatic association from token relationship") } - let tokenRelationshipsProto = try Proto_TokenRelationship.with { proto in - proto.tokenID = TokenId(tokenId)!.toProtobuf() + 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 } - - tokenBalances.append(tokenRelationshipsProto) } - return tokenBalances + return tokenRelationships } internal func getTokenKycStatusFromString(_ tokenKycStatusString: String) throws -> Proto_TokenKycStatus { @@ -192,7 +117,7 @@ internal final class MirrorNodeService { case "NOT_APPLICABLE": return Proto_TokenKycStatus.kycNotApplicable case "GRANTED": return Proto_TokenKycStatus.granted case "REVOKED": return Proto_TokenKycStatus.revoked - case _: fatalError("Invalid token KYC status: \(tokenKycStatusString)") + case _: throw HError.mirrorNodeQuery("Error while processing kyc status from token relationship") } } @@ -201,7 +126,7 @@ internal final class MirrorNodeService { case "NOT_APPLICABLE": return Proto_TokenFreezeStatus.freezeNotApplicable case "FROZEN": return Proto_TokenFreezeStatus.frozen case "UNFROZEN": return Proto_TokenFreezeStatus.unfrozen - case _: fatalError("Invalid token Freeze status: \(tokenFreezeStatusString)") + case _: throw HError.mirrorNodeQuery("Error while processing freeze status from token relationship") } } diff --git a/Sources/Hedera/PingQuery.swift b/Sources/Hedera/PingQuery.swift index a2e4040b..b2fef278 100644 --- a/Sources/Hedera/PingQuery.swift +++ b/Sources/Hedera/PingQuery.swift @@ -73,7 +73,10 @@ extension PingQuery: Execute { nil } - internal func makeRequest(_ client: Client, _ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> ( + internal func makeRequest( + _ ledgerId: LedgerId?, _ mirrorNodeNetworks: [String], _ transactionId: TransactionId?, + _ nodeAccountId: AccountId + ) throws -> ( Proto_Query, () ) { let header = Proto_QueryHeader.with { $0.responseType = .answerOnly } diff --git a/Sources/Hedera/Query.swift b/Sources/Hedera/Query.swift index 048955c4..491ecf22 100644 --- a/Sources/Hedera/Query.swift +++ b/Sources/Hedera/Query.swift @@ -224,7 +224,10 @@ extension Query: Execute { payment.index } - internal func makeRequest(_ client: Client, _ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> ( + internal func makeRequest( + _ ledgerId: LedgerId?, _ mirrorNodeNetworks: [String], _ transactionId: TransactionId?, + _ nodeAccountId: AccountId + ) throws -> ( Proto_Query, Context ) { let request = toQueryProtobufWith( @@ -232,13 +235,14 @@ extension Query: Execute { proto.responseType = .answerOnly if requiresPayment { - proto.payment = try payment.makeRequest(client, transactionId, nodeAccountId).0 + proto.payment = try payment.makeRequest(ledgerId, mirrorNodeNetworks, transactionId, nodeAccountId) + .0 } }) let context = MirrorNetworkContext( - ledgerId: client.ledgerId, - mirrorNetworkNodes: client.mirrorNetwork + ledgerId: ledgerId, + mirrorNetworkNodes: mirrorNodeNetworks ) return (request, context) diff --git a/Sources/Hedera/QueryCost.swift b/Sources/Hedera/QueryCost.swift index 8bde5c7d..fd27b1e8 100644 --- a/Sources/Hedera/QueryCost.swift +++ b/Sources/Hedera/QueryCost.swift @@ -67,7 +67,10 @@ extension QueryCost: Execute { internal var requiresTransactionId: Bool { false } - internal func makeRequest(_ client: Client, _ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> ( + internal func makeRequest( + _ ledgerId: LedgerId?, _ mirrorNodeNetworks: [String], _ transactionId: TransactionId?, + _ nodeAccountId: AccountId + ) throws -> ( Proto_Query, () ) { let request = query.toQueryProtobufWith( diff --git a/Sources/Hedera/Token/TokenRelationship.swift b/Sources/Hedera/Token/TokenRelationship.swift index c5ae1735..61bdcfe6 100644 --- a/Sources/Hedera/Token/TokenRelationship.swift +++ b/Sources/Hedera/Token/TokenRelationship.swift @@ -41,12 +41,12 @@ public struct TokenRelationship: Sendable { /// The KYC status of the account (KycNotApplicable, Granted or Revoked). /// /// If the token does not have KYC key, KycNotApplicable is returned - public let kycStatus: Bool? + 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: Bool? + public let freezeStatus: UInt32 /// Specifies if the relationship is created implicitly. /// @@ -55,7 +55,7 @@ public struct TokenRelationship: Sendable { public let automaticAssociation: Bool public init( - tokenId: TokenId, symbol: String, balance: UInt64, kycStatus: Bool?, freezeStatus: Bool?, + tokenId: TokenId, symbol: String, balance: UInt64, kycStatus: UInt32, freezeStatus: UInt32, automaticAssociation: Bool ) { self.tokenId = tokenId @@ -72,29 +72,29 @@ extension TokenRelationship: TryProtobufCodable { internal typealias Protobuf = Proto_TokenRelationship internal init(protobuf proto: Protobuf) throws { - var freezeStatus: Bool? - var kycStatus: Bool? + var freezeStatus: UInt32 + var kycStatus: UInt32 switch proto.freezeStatus { case .freezeNotApplicable: - freezeStatus = nil + freezeStatus = 0 case .frozen: - freezeStatus = true + freezeStatus = 1 case .unfrozen: - freezeStatus = false + freezeStatus = 2 case .unrecognized(_): - fatalError("Unrecognized Freeze Status from Protobuf: \(proto.freezeStatus)") + throw HError.fromProtobuf("invalid freeze status from protobuf: \(proto.freezeStatus)") } switch proto.kycStatus { case .kycNotApplicable: - kycStatus = nil + kycStatus = 0 case .granted: - kycStatus = true + kycStatus = 1 case .revoked: - kycStatus = false + kycStatus = 2 case .unrecognized(_): - fatalError("Unrecognized KYC Status from protobuf: \(proto.kycStatus)") + throw HError.fromProtobuf("invalid kyc status from protobuf: \(proto.kycStatus)") } self.init( @@ -109,24 +109,24 @@ extension TokenRelationship: TryProtobufCodable { var protoKycStatus: Proto_TokenKycStatus switch freezeStatus { - case true: - protoFreezeStatus = Proto_TokenFreezeStatus.frozen - case false: - protoFreezeStatus = Proto_TokenFreezeStatus.unfrozen - case nil: - protoFreezeStatus = Proto_TokenFreezeStatus.freezeNotApplicable - case .some(_): + case 0: + protoFreezeStatus = .freezeNotApplicable + case 1: + protoFreezeStatus = .frozen + case 2: + protoFreezeStatus = .unfrozen + default: fatalError("Unrecognized Freeze Status") } switch kycStatus { - case true: - protoKycStatus = Proto_TokenKycStatus.granted - case false: - protoKycStatus = Proto_TokenKycStatus.revoked - case nil: - protoKycStatus = Proto_TokenKycStatus.kycNotApplicable - case .some(_): + case 0: + protoKycStatus = .kycNotApplicable + case 1: + protoKycStatus = .granted + case 2: + protoKycStatus = .revoked + default: fatalError("Unrecognized KYC Status") } diff --git a/Sources/Hedera/Transaction.swift b/Sources/Hedera/Transaction.swift index bc84033a..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(_ client: Client, _ 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 cc516120..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(_ client: Client, _ 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/Tests/HederaE2ETests/Account/AccountBalance.swift b/Tests/HederaE2ETests/Account/AccountBalance.swift index fb469c7a..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 { diff --git a/Tests/HederaE2ETests/Contract/ContractIdPopulation.swift b/Tests/HederaE2ETests/Contract/ContractIdPopulation.swift deleted file mode 100644 index aeb0f3df..00000000 --- a/Tests/HederaE2ETests/Contract/ContractIdPopulation.swift +++ /dev/null @@ -1,59 +0,0 @@ -/* - * ‌ - * 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 Hedera -import XCTest - -internal final class ContractIdPopulation: XCTestCase { - internal let contractByteCode = - "608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506101cb806100606000396000f3fe608060405260043610610046576000357c01000000000000000000000000000000000000000000000000000000009004806341c0e1b51461004b578063cfae321714610062575b600080fd5b34801561005757600080fd5b506100606100f2565b005b34801561006e57600080fd5b50610077610162565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100b757808201518184015260208101905061009c565b50505050905090810190601f1680156100e45780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610160573373ffffffffffffffffffffffffffffffffffffffff16ff5b565b60606040805190810160405280600d81526020017f48656c6c6f2c20776f726c64210000000000000000000000000000000000000081525090509056fea165627a7a72305820ae96fb3af7cde9c0abfe365272441894ab717f816f07f41f07b1cbede54e256e0029" - .data(using: .utf8)! - - internal func testPopulateContractIdNum() async throws { - let testEnv = try TestEnvironment.nonFree - - let fileCreateReceipt = try await FileCreateTransaction() - .keys([.single(testEnv.operator.privateKey.publicKey)]) - .contents(self.contractByteCode) - .execute(testEnv.client) - .getReceipt(testEnv.client) - - let fileId = try XCTUnwrap(fileCreateReceipt.fileId) - - let contractCreateReceipt = try await ContractCreateTransaction() - .adminKey(.single(testEnv.operator.privateKey.publicKey)) - .gas(100000) - .constructorParameters(ContractFunctionParameters().addString("Hello from Hedera.")) - .contractMemo("[e2e::ContractIdPopulation]") - .bytecodeFileId(fileId) - .execute(testEnv.client) - .getReceipt(testEnv.client) - - let contractId = try XCTUnwrap(contractCreateReceipt.contractId) - try await Task.sleep(nanoseconds: 5 * 1_000_000_000) - - let contractInfo = try await ContractInfoQuery(contractId: contractId).execute(testEnv.client) - let contractIdMirror = try ContractId.fromEvmAddress(0, 0, contractInfo.contractAccountId) - - let newContractId = try await contractIdMirror.populateContractNum(testEnv.client) - - XCTAssertEqual(contractId.num, newContractId.num) - } -} From 83cc8e2ae727fa02b3a61e4d71378ceba00d6e2c Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Wed, 5 Jun 2024 15:06:01 -0700 Subject: [PATCH 17/18] chore: fmt Signed-off-by: Ricky Saechao --- Sources/Hedera/MirrorNodeService.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Hedera/MirrorNodeService.swift b/Sources/Hedera/MirrorNodeService.swift index 83bb7a19..a7f37e07 100644 --- a/Sources/Hedera/MirrorNodeService.swift +++ b/Sources/Hedera/MirrorNodeService.swift @@ -63,7 +63,8 @@ internal final class MirrorNodeService { return tokenBalances } - internal func getTokenRelationshipsForAccount(_ idNumOrEvmAddress: String) async throws -> [Proto_TokenRelationship] { + internal func getTokenRelationshipsForAccount(_ idNumOrEvmAddress: String) async throws -> [Proto_TokenRelationship] + { let accountTokensResponse = try await self.mirrorNodeGateway.getAccountTokens(idNumOrEvmAddress) guard let tokens = accountTokensResponse["tokens"] else { From 768ff36300a26ffa8556aa9eca90f5f0cc443c6b Mon Sep 17 00:00:00 2001 From: Ricky Saechao Date: Wed, 5 Jun 2024 15:25:33 -0700 Subject: [PATCH 18/18] fix: reuse http client for mirror node queries Signed-off-by: Ricky Saechao --- Sources/Hedera/MirrorNodeGateway.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/Hedera/MirrorNodeGateway.swift b/Sources/Hedera/MirrorNodeGateway.swift index 8a555bb5..cd41b661 100644 --- a/Sources/Hedera/MirrorNodeGateway.swift +++ b/Sources/Hedera/MirrorNodeGateway.swift @@ -24,11 +24,17 @@ import GRPC import HederaProtobufs import NIO -internal struct MirrorNodeGateway { +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 { @@ -77,11 +83,6 @@ internal struct MirrorNodeGateway { } private func queryFromMirrorNode(_ apiUrl: String) async throws -> String { - let httpClient = HTTPClient(eventLoopGroupProvider: .singleton) - defer { - try? httpClient.syncShutdown() - } - // 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)