diff --git a/account_balance_query.go b/account_balance_query.go index 245d7b3f5..252aa2531 100644 --- a/account_balance_query.go +++ b/account_balance_query.go @@ -96,7 +96,6 @@ func (q *AccountBalanceQuery) Execute(client *Client) (AccountBalance, error) { if client == nil { return AccountBalance{}, errNoClientProvided } - var err error err = q.validateNetworkOnIDs(client) @@ -113,7 +112,38 @@ func (q *AccountBalanceQuery) Execute(client *Client) (AccountBalance, error) { return AccountBalance{}, err } - return _AccountBalanceFromProtobuf(resp.(*services.Response).GetCryptogetAccountBalance()), nil + result := _AccountBalanceFromProtobuf(resp.(*services.Response).GetCryptogetAccountBalance()) + + network := obtainUrlForMirrorNode(client) + if q.accountID != nil { + err = queryBalanceFromMirrorNode(network, q.accountID.String(), &result) + } else { + err = queryBalanceFromMirrorNode(network, q.contractID.String(), &result) + } + if err != nil { + return AccountBalance{}, nil + } + return result, nil +} + +// Helper function, which query the mirror node and if the balance has tokens, it iterate over the tokens and assign them +// inside `AccountBalance` tokens field +func queryBalanceFromMirrorNode(network string, id string, result *AccountBalance) error { + response, err := accountBalanceQuery(network, id) + if err != nil { + return err + } + // If user has tokens + result.Tokens.balances = make(map[string]uint64) + if tokens, ok := response["tokens"].([]map[string]interface{}); ok { + for _, token := range tokens { + for key, value := range token { + result.Tokens.balances[key] = value.(uint64) + } + } + } + + return nil } // SetMaxQueryPayment sets the maximum payment allowed for this query. diff --git a/account_balance_query_e2e_test.go b/account_balance_query_e2e_test.go index 8c9f198dd..a1e521057 100644 --- a/account_balance_query_e2e_test.go +++ b/account_balance_query_e2e_test.go @@ -24,6 +24,7 @@ package hedera */ import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -194,6 +195,8 @@ func TestIntegrationAccountBalanceQueryNoAccountIDError(t *testing.T) { _, err := NewAccountBalanceQuery(). SetNodeAccountIDs(env.NodeAccountIDs). Execute(env.Client) + + fmt.Println(env.Client.network.ledgerID) assert.Error(t, err) assert.True(t, err.Error() == "exceptional precheck status INVALID_ACCOUNT_ID") diff --git a/account_info_query.go b/account_info_query.go index b68af35d5..cd4d8f0df 100644 --- a/account_info_query.go +++ b/account_info_query.go @@ -21,6 +21,7 @@ package hedera */ import ( + "errors" "time" "github.com/hashgraph/hedera-protobufs-go/services" @@ -56,7 +57,37 @@ func (q *AccountInfoQuery) Execute(client *Client) (AccountInfo, error) { return AccountInfo{}, err } - return _AccountInfoFromProtobuf(resp.GetCryptoGetInfo().AccountInfo) + result, err := _AccountInfoFromProtobuf(resp.GetCryptoGetInfo().AccountInfo) + if err != nil { + return AccountInfo{}, err + } + + network := obtainUrlForMirrorNode(client) + _, err = accountInfoqueryTokensRelationshipFromMirrorNode(network, q.accountID.String(), &result) + + if err != nil { + return AccountInfo{}, err + } + return result, nil +} + +// Helper function, which query the mirror node about tokenRelationship of for all tokens that the account is +// being associated with +func accountInfoqueryTokensRelationshipFromMirrorNode(network string, id string, result *AccountInfo) (*AccountInfo, error) { + response, err := tokenReleationshipQuery(network, id) + if err != nil { + return result, err + } + tokens, ok := response["tokens"].([]interface{}) + if !ok { + return result, errors.New("Ivalid tokens format") + } + mappedTokens, err := mapTokenRelationship(tokens) + if err != nil { + return &AccountInfo{}, err + } + result.TokenRelationships = mappedTokens + return result, nil } // SetGrpcDeadline When execution is attempted, a single attempt will timeout when this deadline is reached. (The SDK may subsequently retry the execution.) diff --git a/account_info_query_e2e_test.go b/account_info_query_e2e_test.go index 0d659303a..767387aa8 100644 --- a/account_info_query_e2e_test.go +++ b/account_info_query_e2e_test.go @@ -24,9 +24,9 @@ package hedera */ import ( - "testing" - "github.com/stretchr/testify/assert" + "testing" + "time" "github.com/stretchr/testify/require" ) @@ -52,6 +52,7 @@ func TestIntegrationAccountInfoQueryCanExecute(t *testing.T) { accountID := *receipt.AccountID require.NoError(t, err) + time.Sleep(3 * time.Second) info, err := NewAccountInfoQuery(). SetAccountID(accountID). @@ -82,8 +83,8 @@ func TestIntegrationAccountInfoQueryCanExecute(t *testing.T) { _, err = resp.SetValidateStatus(true).GetReceipt(env.Client) require.NoError(t, err) - //err = CloseIntegrationTestEnv(env, nil) - //require.NoError(t, err) + err = CloseIntegrationTestEnv(env, nil) + require.NoError(t, err) } func TestIntegrationAccountInfoQueryGetCost(t *testing.T) { @@ -116,7 +117,7 @@ func TestIntegrationAccountInfoQueryGetCost(t *testing.T) { cost, err := accountInfo.GetCost(env.Client) require.NoError(t, err) - + time.Sleep(3 * time.Second) info, err := accountInfo.SetQueryPayment(cost).Execute(env.Client) require.NoError(t, err) @@ -230,6 +231,7 @@ func TestIntegrationAccountInfoQuerySetBigMaxPayment(t *testing.T) { _, err = accountInfo.GetCost(env.Client) require.NoError(t, err) + time.Sleep(3 * time.Second) info, err := accountInfo.SetQueryPayment(NewHbar(1)).Execute(env.Client) require.NoError(t, err) diff --git a/contract_info.go b/contract_info.go index 2b4278be1..91b10628e 100644 --- a/contract_info.go +++ b/contract_info.go @@ -29,15 +29,17 @@ import ( // Current information on the smart contract instance, including its balance. type ContractInfo struct { - AccountID AccountID - ContractID ContractID - ContractAccountID string - AdminKey Key - ExpirationTime time.Time - AutoRenewPeriod time.Duration - Storage uint64 - ContractMemo string - Balance uint64 + AccountID AccountID + ContractID ContractID + ContractAccountID string + AdminKey Key + ExpirationTime time.Time + AutoRenewPeriod time.Duration + Storage uint64 + ContractMemo string + Balance uint64 + // Deprecated + TokenRelationships []*TokenRelationship LedgerID LedgerID AutoRenewAccountID *AccountID MaxAutomaticTokenAssociations int32 diff --git a/contract_info_query.go b/contract_info_query.go index dec0ad256..65e9b3f04 100644 --- a/contract_info_query.go +++ b/contract_info_query.go @@ -21,6 +21,7 @@ package hedera */ import ( + "errors" "time" "github.com/hashgraph/hedera-protobufs-go/services" @@ -80,10 +81,34 @@ func (q *ContractInfoQuery) Execute(client *Client) (ContractInfo, error) { if err != nil { return ContractInfo{}, err } + network := obtainUrlForMirrorNode(client) + _, err = contractInfoqueryTokensRelationshipFromMirrorNode(network, q.contractID.String(), &info) + if err != nil { + return ContractInfo{}, err + } return info, nil } +// Helper function, which query the mirror node about tokenRelationship of for all tokens that the account is +// being associated with +func contractInfoqueryTokensRelationshipFromMirrorNode(network string, id string, result *ContractInfo) (*ContractInfo, error) { + response, err := tokenReleationshipQuery(network, id) + if err != nil { + return result, err + } + tokens, ok := response["tokens"].([]interface{}) + if !ok { + return result, errors.New("Ivalid tokens format") + } + mappedTokens, err := mapTokenRelationship(tokens) + if err != nil { + return &ContractInfo{}, err + } + result.TokenRelationships = mappedTokens + return result, nil +} + // SetMaxQueryPayment sets the maximum payment allowed for this Query. func (q *ContractInfoQuery) SetMaxQueryPayment(maxPayment Hbar) *ContractInfoQuery { q.Query.SetMaxQueryPayment(maxPayment) diff --git a/mirror_node_gateway.go b/mirror_node_gateway.go new file mode 100644 index 000000000..6062bc9c4 --- /dev/null +++ b/mirror_node_gateway.go @@ -0,0 +1,98 @@ +package hedera + +import ( + "encoding/json" + "fmt" + "net/http" +) + +/*- + * + * Hedera Go SDK + * + * Copyright (C) 2020 - 2023 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. + * + */ +const httpsPrefix = "https://" +const apiPathVersion = "/api/v1" + +var queryTypes = map[string]string{ + "account": "accounts", + "contract": "contracts", + "token": "tokens"} + +// Function to obtain balance of tokens for given account ID. Return the pure JSON response as mapping +func accountBalanceQuery(network string, accountId string) (map[string]interface{}, error) { + info, err := accountInfoQuery(network, accountId) + // Cast balance body to map + return info["balance"].(map[string]interface{}), err +} + +// Function to obtain account info for given account ID. Return the pure JSON response as mapping +func accountInfoQuery(network string, accountId string) (map[string]interface{}, error) { + accountInfoUrl := buildUrl(network, queryTypes["account"], accountId) + return makeGetRequest(accountInfoUrl) +} + +// Function to obtain balance of tokens for given contract ID. Return the pure JSON response as mapping +func contractInfoQuery(network string, contractId string) (map[string]interface{}, error) { // nolint + contractInfoUrl := buildUrl(network, queryTypes["contract"], contractId) + return makeGetRequest(contractInfoUrl) +} + +func tokenReleationshipQuery(network string, id string) (map[string]interface{}, error) { + tokenRelationshipUrl := buildUrl(network, queryTypes["account"], id, queryTypes["token"]) + return makeGetRequest(tokenRelationshipUrl) +} + +// Make a GET HTTP request to provided URL and map it's json response to a generic `interface` map and return it +func makeGetRequest(url string) (response map[string]interface{}, e error) { + // Make an HTTP request + resp, err := http.Get(url) //nolint + + if err != nil { + return nil, err + } + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("HTTP request failed with status code: %d", resp.StatusCode) + } + defer resp.Body.Close() + + // Decode the JSON response into a map + var resultMap map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&resultMap) + if err != nil { + return nil, err + } + + return resultMap, nil +} + +func obtainUrlForMirrorNode(client *Client) string { + const localNetwork = "127.0.0.1" + if client.GetMirrorNetwork()[0] == localNetwork+":5600" || client.GetMirrorNetwork()[0] == localNetwork+":443" { + return localNetwork + "5551" + } else { + return client.GetMirrorNetwork()[0] + } +} + +func buildUrl(network string, args ...string) string { + url := httpsPrefix + network + apiPathVersion + for _, arg := range args { + url += "/" + arg + } + return url +} diff --git a/mirror_node_gateway_e2e_test.go b/mirror_node_gateway_e2e_test.go new file mode 100644 index 000000000..ff75337da --- /dev/null +++ b/mirror_node_gateway_e2e_test.go @@ -0,0 +1,96 @@ +//go:build all || e2e +// +build all e2e + +package hedera + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +/*- + * + * Hedera Go SDK + * + * Copyright (C) 2020 - 2023 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. + * + */ +const mainnetMirrorNodeUrl = "mainnet-public.mirrornode.hedera.com" +const testnetMirrorNodeUrl = "testnet.mirrornode.hedera.com" +const previewnetMirrorNodeUrl = "previewnet.mirrornode.hedera.com" + +func TestAccountInfoTestnet(t *testing.T) { + testAccountInfoQuery(t, testnetMirrorNodeUrl) +} +func TestAccountInfoMainnet(t *testing.T) { + testAccountInfoQuery(t, mainnetMirrorNodeUrl) +} +func TestAccountInfoPreviewnet(t *testing.T) { + testAccountInfoQuery(t, previewnetMirrorNodeUrl) +} +func testAccountInfoQuery(t *testing.T, network string) { + t.Parallel() + + result, e := accountInfoQuery(network, "1") + require.NoError(t, e) + assert.Equal(t, 20, len(result)) +} + +func TestAccountBalanceTestnet(t *testing.T) { + testAccountBalanceQuery(t, testnetMirrorNodeUrl) +} +func TestAccountBalanceMainnet(t *testing.T) { + testAccountBalanceQuery(t, mainnetMirrorNodeUrl) +} +func TestAccountBalancePreviewnet(t *testing.T) { + testAccountBalanceQuery(t, previewnetMirrorNodeUrl) +} +func testAccountBalanceQuery(t *testing.T, network string) { + t.Parallel() + + result, e := accountBalanceQuery(network, "1") + require.NoError(t, e) + assert.Equal(t, 3, len(result)) + _, exist := result["balance"] + require.True(t, exist) + _, exist = result["timestamp"] + require.True(t, exist) + _, exist = result["tokens"] + require.True(t, exist) +} + +func TestContractInfoPreviewnetContractNotFound(t *testing.T) { + t.Parallel() + + result, e := contractInfoQuery(previewnetMirrorNodeUrl, "1") + require.Error(t, e) + assert.True(t, result == nil) +} +func TestContractInfoTestnet(t *testing.T) { + t.Parallel() + + result, e := contractInfoQuery(testnetMirrorNodeUrl, "0.0.7376843") + require.NoError(t, e) + _, exist := result["bytecode"] + require.True(t, exist) +} + +func TestBuildUrlReturnCorrectUrl(t *testing.T) { + url := "https://testnet.mirrornode.hedera.com/api/v1/accounts/0.0.7477022/tokens" + + result := buildUrl("testnet.mirrornode.hedera.com", "accounts", "0.0.7477022", "tokens") + assert.Equal(t, url, result) +} diff --git a/token_relationship.go b/token_relationship.go index 00b92dc41..387beeb36 100644 --- a/token_relationship.go +++ b/token_relationship.go @@ -1,5 +1,7 @@ package hedera +import "errors" + /*- * * Hedera Go SDK @@ -31,77 +33,114 @@ type TokenRelationship struct { AutomaticAssociation bool } -// func _TokenRelationshipFromProtobuf(pb *services.TokenRelationship) TokenRelationship { -// if pb == nil { -// return TokenRelationship{} -// } +// func _TokenRelationshipFromProtobuf(pb *services.TokenRelationship) TokenRelationship { +// if pb == nil { +// return TokenRelationship{} +// } // -// tokenID := TokenID{} -// if pb.TokenId != nil { -// tokenID = *_TokenIDFromProtobuf(pb.TokenId) -// } +// tokenID := TokenID{} +// if pb.TokenId != nil { +// tokenID = *_TokenIDFromProtobuf(pb.TokenId) +// } // -// return TokenRelationship{ -// TokenID: tokenID, -// Symbol: pb.Symbol, -// Balance: pb.Balance, -// KycStatus: _KycStatusFromProtobuf(pb.KycStatus), -// FreezeStatus: _FreezeStatusFromProtobuf(pb.FreezeStatus), -// Decimals: pb.Decimals, -// AutomaticAssociation: pb.AutomaticAssociation, +// return TokenRelationship{ +// TokenID: tokenID, +// Symbol: pb.Symbol, +// Balance: pb.Balance, +// KycStatus: _KycStatusFromProtobuf(pb.KycStatus), +// FreezeStatus: _FreezeStatusFromProtobuf(pb.FreezeStatus), +// Decimals: pb.Decimals, +// AutomaticAssociation: pb.AutomaticAssociation, +// } // } -//} // -// func (relationship *TokenRelationship) _ToProtobuf() *services.TokenRelationship { -// var freezeStatus services.TokenFreezeStatus -// switch *relationship.FreezeStatus { -// case true: -// freezeStatus = 1 -// case false: -// freezeStatus = 2 -// default: -// freezeStatus = 0 -// } +// func (relationship *TokenRelationship) _ToProtobuf() *services.TokenRelationship { +// var freezeStatus services.TokenFreezeStatus +// switch *relationship.FreezeStatus { +// case true: +// freezeStatus = 1 +// case false: +// freezeStatus = 2 +// default: +// freezeStatus = 0 +// } // -// var kycStatus services.TokenKycStatus -// switch *relationship.KycStatus { -// case true: -// kycStatus = 1 -// case false: -// kycStatus = 2 -// default: -// kycStatus = 0 -// } +// var kycStatus services.TokenKycStatus +// switch *relationship.KycStatus { +// case true: +// kycStatus = 1 +// case false: +// kycStatus = 2 +// default: +// kycStatus = 0 +// } // -// return &services.TokenRelationship{ -// TokenId: relationship.TokenID._ToProtobuf(), -// Symbol: relationship.Symbol, -// Balance: relationship.Balance, -// KycStatus: kycStatus, -// FreezeStatus: freezeStatus, -// Decimals: relationship.Decimals, -// AutomaticAssociation: relationship.AutomaticAssociation, +// return &services.TokenRelationship{ +// TokenId: relationship.TokenID._ToProtobuf(), +// Symbol: relationship.Symbol, +// Balance: relationship.Balance, +// KycStatus: kycStatus, +// FreezeStatus: freezeStatus, +// Decimals: relationship.Decimals, +// AutomaticAssociation: relationship.AutomaticAssociation, +// } // } -//} // -// func (relationship TokenRelationship) ToBytes() []byte { -// data, err := protobuf.Marshal(relationship._ToProtobuf()) -// if err != nil { -// return make([]byte, 0) +// func (relationship TokenRelationship) ToBytes() []byte { +// data, err := protobuf.Marshal(relationship._ToProtobuf()) +// if err != nil { +// return make([]byte, 0) +// } +// +// return data // } // -// return data -//} +// func TokenRelationshipFromBytes(data []byte) (TokenRelationship, error) { +// if data == nil { +// return TokenRelationship{}, errors.New("byte array can't be null") +// } +// pb := services.TokenRelationship{} +// err := protobuf.Unmarshal(data, &pb) +// if err != nil { +// return TokenRelationship{}, err +// } // -// func TokenRelationshipFromBytes(data []byte) (TokenRelationship, error) { -// if data == nil { -// return TokenRelationship{}, errors.New("byte array can't be null") +// return _TokenRelationshipFromProtobuf(&pb), nil // } -// pb := services.TokenRelationship{} -// err := protobuf.Unmarshal(data, &pb) -// if err != nil { -// return TokenRelationship{}, err -// } -// -// return _TokenRelationshipFromProtobuf(&pb), nil -//} +func mapTokenRelationship(tokens []interface{}) ([]*TokenRelationship, error) { + var tokenRelationships []*TokenRelationship + + for _, tokenObj := range tokens { + token, ok := tokenObj.(map[string]interface{}) + if !ok { + return nil, errors.New("invalid token object") + } + + tokenId, err := TokenIDFromString(token["token_id"].(string)) + if err != nil { + return nil, err + } + + freezeStatus := false + if tokenFreezeStatus, ok := token["freeze_status"].(string); ok { + freezeStatus = tokenFreezeStatus == "FROZEN" + } + + kycStatus := false + if tokenKycStatus, ok := token["kyc_status"].(string); ok { + kycStatus = tokenKycStatus == "GRANTED" + } + + tokenRelationship := &TokenRelationship{ + TokenID: tokenId, + Balance: uint64(token["balance"].(float64)), + KycStatus: &kycStatus, + FreezeStatus: &freezeStatus, + AutomaticAssociation: token["automatic_association"].(bool), + } + + tokenRelationships = append(tokenRelationships, tokenRelationship) + } + + return tokenRelationships, nil +}