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 15 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,12 +85,22 @@ 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 {
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))

return AccountBalance(
accountId: accountId, hbars: .fromTinybars(Int64(proto.balance)),
tokensInner: .fromProtobuf(tokenBalanceProto))
}

public override func validateChecksums(on ledgerId: LedgerId) throws {
Expand Down
9 changes: 9 additions & 0 deletions Sources/Hedera/Account/AccountId.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ public struct AccountId: Sendable, EntityId, ValidateChecksums {
public func toBytes() -> Data {
toProtobufBytes()
}

public func populateAccountIdNum(_ client: Client) async throws -> Self {
RickyLB marked this conversation as resolved.
Show resolved Hide resolved
let mirrorNodeGateway = try MirrorNodeGateway.forClient(client)
let mirrorNodeService = MirrorNodeService(mirrorNodeGateway)

let accountNumFromMirror = try await mirrorNodeService.getAccountNum(self.evmAddress!.toString())

RickyLB marked this conversation as resolved.
Show resolved Hide resolved
return Self(shard: shard, realm: realm, num: accountNumFromMirror)
}
RickyLB marked this conversation as resolved.
Show resolved Hide resolved
}

extension AccountId: TryProtobufCodable {
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?

/// Staking metadata for this account.
RickyLB marked this conversation as resolved.
Show resolved Hide resolved
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
40 changes: 38 additions & 2 deletions Sources/Hedera/Account/AccountInfoQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,48 @@ 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 {
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 accountInfo = try AccountInfo.fromProtobuf(proto.accountInfo)
let tokenRelationshipsProto = try await mirrorNodeService.getTokenRelationshipsForAccount(
String(describing: accountInfo.accountId.num))

var tokenRelationships: [TokenId: TokenRelationship] = [:]

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
)
RickyLB marked this conversation as resolved.
Show resolved Hide resolved
}

internal override func validateChecksums(on ledgerId: LedgerId) throws {
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
4 changes: 2 additions & 2 deletions Sources/Hedera/ChunkedTransaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
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
11 changes: 11 additions & 0 deletions Sources/Hedera/Contract/ContractId.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ 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)
}
RickyLB marked this conversation as resolved.
Show resolved Hide resolved
}

#if compiler(>=5.7)
Expand Down
14 changes: 13 additions & 1 deletion 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
///
/// Query mirror node
RickyLB marked this conversation as resolved.
Show resolved Hide resolved
public let tokenRelationships: [TokenId: TokenRelationship]

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

Expand Down Expand Up @@ -91,9 +96,15 @@ 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),
accountId: try AccountId.fromProtobuf(proto.accountID),
RickyLB marked this conversation as resolved.
Show resolved Hide resolved
contractAccountId: proto.contractAccountID,
adminKey: try .fromProtobuf(adminKey),
expirationTime: .fromProtobuf(expirationTime),
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
Loading