Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: query mirror node for AccountBalances, AccountInfo, ContractInfo #343

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"
}
},
{
Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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: [
Expand Down
12 changes: 11 additions & 1 deletion Sources/Hedera/Account/AccountBalance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) })
Expand Down
14 changes: 12 additions & 2 deletions Sources/Hedera/Account/AccountBalanceQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,21 @@ public final class AccountBalanceQuery: Query<AccountBalance> {
try await Proto_CryptoServiceAsyncClient(channel: channel).cryptoGetBalance(request)
}

internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response {
guard case .cryptogetAccountBalance(let proto) = response else {
internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws
-> Response
{
let mirrorNodeGateway = try MirrorNodeGateway.forNetwork(context.mirrorNetworkNodes, context.ledgerId)
let mirrorNodeService = MirrorNodeService.init(mirrorNodeGateway)

guard case .cryptogetAccountBalance(var proto) = response else {
throw HError.fromProtobuf("unexpected \(response) received, expected `cryptogetAccountBalance`")
}

let tokenBalanceProto = try await mirrorNodeService.getTokenBalancesForAccount(
String(proto.accountID.accountNum))

proto.tokenBalances = tokenBalanceProto

return try .fromProtobuf(proto)
}

Expand Down
11 changes: 11 additions & 0 deletions Sources/Hedera/Account/AccountInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public struct AccountInfo: Sendable {
maxAutomaticTokenAssociations: UInt32,
aliasKey: PublicKey?,
ethereumNonce: UInt64,
tokenRelationships: [TokenId: TokenRelationship],
ledgerId: LedgerId,
staking: StakingInfo?
) {
Expand All @@ -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,
Expand Down Expand Up @@ -154,6 +156,9 @@ public struct AccountInfo: Sendable {
/// Staking metadata for this account.
public let staking: StakingInfo?

/// Token relationships for this account.
public let tokenRelationships: [TokenId: TokenRelationship]

/// Decode `Self` from protobuf-encoded `bytes`.
///
/// - Throws: ``HError/ErrorKind/fromProtobuf`` if:
Expand All @@ -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,
Expand All @@ -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)
)
Expand Down
14 changes: 12 additions & 2 deletions Sources/Hedera/Account/AccountInfoQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,21 @@ public final class AccountInfoQuery: Query<AccountInfo> {
try await Proto_CryptoServiceAsyncClient(channel: channel).getAccountInfo(request)
}

internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response {
guard case .cryptoGetInfo(let proto) = response else {
internal override func makeQueryResponse(_ context: Context, _ response: Proto_Response.OneOf_Response) async throws
-> Response
{
let mirrorNodeGateway = try MirrorNodeGateway.forNetwork(context.mirrorNetworkNodes, context.ledgerId)
let mirrorNodeService = MirrorNodeService.init(mirrorNodeGateway)

guard case .cryptoGetInfo(var proto) = response else {
throw HError.fromProtobuf("unexpected \(response) received, expected `cryptoGetInfo`")
}

let tokenRelationshipsProto = try await mirrorNodeService.getTokenRelationshipsForAccount(
String(describing: try AccountId.fromProtobuf(proto.accountInfo.accountID).num))

proto.accountInfo.tokenRelationships = tokenRelationshipsProto

return try .fromProtobuf(proto.accountInfo)
}

Expand Down
4 changes: 3 additions & 1 deletion Sources/Hedera/Account/AccountRecordsQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`")
}
Expand Down
4 changes: 3 additions & 1 deletion Sources/Hedera/Account/AccountStakersQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`")
}
Expand Down
10 changes: 8 additions & 2 deletions Sources/Hedera/ChunkedTransaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,10 @@ extension ChunkedTransaction.FirstChunkView: Execute {
self.transaction.regenerateTransactionId
}

internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> (
internal func makeRequest(
_ ledgerId: LedgerId?, _ mirrorNodeNetworks: [String], _ transactionId: TransactionId?,
_ nodeAccountId: AccountId
) throws -> (
GrpcRequest, Context
) {
assert(transaction.isFrozen)
Expand Down Expand Up @@ -265,7 +268,10 @@ extension ChunkedTransaction.ChunkView: Execute {
self.transaction.regenerateTransactionId
}

internal func makeRequest(_ transactionId: TransactionId?, _ nodeAccountId: AccountId) throws -> (
internal func makeRequest(
_ ledgerId: LedgerId?, _ mirrorNodeNetworks: [String], _ transactionId: TransactionId?,
_ nodeAccountId: AccountId
) throws -> (
GrpcRequest, Context
) {
assert(transaction.isFrozen)
Expand Down
4 changes: 3 additions & 1 deletion Sources/Hedera/Contract/ContractBytecodeQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ public final class ContractBytecodeQuery: Query<Data> {
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`")
}
Expand Down
4 changes: 3 additions & 1 deletion Sources/Hedera/Contract/ContractCallQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ public final class ContractCallQuery: Query<ContractFunctionResult> {
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`")
}
Expand Down
12 changes: 12 additions & 0 deletions Sources/Hedera/Contract/ContractInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public struct ContractInfo {
/// The maximum number of tokens that a contract can be implicitly associated with.
public let maxAutomaticTokenAssociations: UInt32

/// The tokens associated to the contract
///
/// Note: Query mirror node for token relationships.
public let tokenRelationships: [TokenId: TokenRelationship]

/// Ledger ID for the network the response was returned from.
public let ledgerId: LedgerId

Expand Down Expand Up @@ -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),
Expand All @@ -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)
)
Expand Down
18 changes: 14 additions & 4 deletions Sources/Hedera/Contract/ContractInfoQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,27 @@ public final class ContractInfoQuery: Query<ContractInfo> {
try await Proto_SmartContractServiceAsyncClient(channel: channel).getContractInfo(request)
}

internal override func makeQueryResponse(_ response: Proto_Response.OneOf_Response) throws -> Response {
guard case .contractGetInfo(let proto) = response else {
internal override func makeQueryResponse(_ context: MirrorNetworkContext, _ response: Proto_Response.OneOf_Response)
async throws
-> Response
{
let mirrorNodeGateway = try MirrorNodeGateway.forNetwork(context.mirrorNetworkNodes, context.ledgerId)
let mirrorNodeService = MirrorNodeService.init(mirrorNodeGateway)

guard case .contractGetInfo(var proto) = response else {
throw HError.fromProtobuf("unexpected \(response) received, expected `contractGetInfo`")
}

return try .fromProtobuf(proto.contractInfo)
let tokenRelationshipsProto = try await mirrorNodeService.getTokenRelationshipsForAccount(
String(describing: proto.contractInfo.accountID.accountNum))

proto.contractInfo.tokenRelationships = tokenRelationshipsProto

return try ContractInfo.fromProtobuf(proto.contractInfo)
}

internal override func validateChecksums(on ledgerId: LedgerId) throws {
try contractId?.validateChecksums(on: ledgerId)
try super.validateChecksums(on: ledgerId)
}

}
Loading