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)