All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hedera.node.app.service.token.impl.handlers.CryptoGetAccountBalanceHandler Maven / Gradle / Ivy

There is a newer version: 0.57.3
Show newest version
/*
 * 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.
 */

package com.hedera.node.app.service.token.impl.handlers;

import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_DELETED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.CONTRACT_DELETED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_CONTRACT_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.OK;
import static com.hedera.node.app.spi.validation.Validations.mustExist;
import static com.hedera.node.app.spi.validation.Validations.validateAccountID;
import static com.hedera.node.app.spi.workflows.PreCheckException.validateFalsePreCheck;
import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck;
import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.ContractID;
import com.hedera.hapi.node.base.HederaFunctionality;
import com.hedera.hapi.node.base.QueryHeader;
import com.hedera.hapi.node.base.ResponseHeader;
import com.hedera.hapi.node.base.TokenBalance;
import com.hedera.hapi.node.base.TokenID;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.hapi.node.state.token.Token;
import com.hedera.hapi.node.state.token.TokenRelation;
import com.hedera.hapi.node.token.CryptoGetAccountBalanceQuery;
import com.hedera.hapi.node.token.CryptoGetAccountBalanceResponse;
import com.hedera.hapi.node.transaction.Query;
import com.hedera.hapi.node.transaction.Response;
import com.hedera.node.app.service.token.ReadableAccountStore;
import com.hedera.node.app.service.token.ReadableTokenRelationStore;
import com.hedera.node.app.service.token.ReadableTokenStore;
import com.hedera.node.app.spi.workflows.FreeQueryHandler;
import com.hedera.node.app.spi.workflows.PreCheckException;
import com.hedera.node.app.spi.workflows.QueryContext;
import com.hedera.node.config.data.TokensConfig;
import com.swirlds.common.metrics.SpeedometerMetric;
import com.swirlds.metrics.api.Metrics;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * This class contains all workflow-related functionality regarding {@link
 * HederaFunctionality#CRYPTO_GET_ACCOUNT_BALANCE}.
 */
@Singleton
public class CryptoGetAccountBalanceHandler extends FreeQueryHandler {

    private static final SpeedometerMetric.Config BALANCE_SPEEDOMETER_CONFIG = new SpeedometerMetric.Config(
                    "app", "queriedAccountBalances")
            .withDescription("Number of balances requested in GetAccountBalance queries per second");

    private final SpeedometerMetric balanceSpeedometer;

    /**
     * Default constructor for injection.
     */
    @Inject
    public CryptoGetAccountBalanceHandler(@NonNull final Metrics metrics) {
        this.balanceSpeedometer = metrics.getOrCreate(BALANCE_SPEEDOMETER_CONFIG);
    }

    @Override
    public QueryHeader extractHeader(@NonNull final Query query) {
        requireNonNull(query);
        return query.cryptogetAccountBalanceOrThrow().header();
    }

    @Override
    public Response createEmptyResponse(@NonNull final ResponseHeader header) {
        requireNonNull(header);
        final var response = CryptoGetAccountBalanceResponse.newBuilder().header(requireNonNull(header));
        return Response.newBuilder().cryptogetAccountBalance(response).build();
    }

    @Override
    // contract.deleted() won't throw NPE since we are checking it for null the line before
    @SuppressWarnings("java:S2259")
    public void validate(@NonNull final QueryContext context) throws PreCheckException {
        requireNonNull(context);
        final var query = context.query();
        final var accountStore = context.createStore(ReadableAccountStore.class);
        final CryptoGetAccountBalanceQuery op = query.cryptogetAccountBalanceOrThrow();
        if (op.hasAccountID()) {
            validateAccountId(op, accountStore);
        } else if (op.hasContractID()) {
            validateContractId(op, accountStore);
        } else {
            throw new PreCheckException(INVALID_ACCOUNT_ID);
        }
    }

    private void validateContractId(CryptoGetAccountBalanceQuery op, ReadableAccountStore accountStore)
            throws PreCheckException {
        mustExist(op.contractID(), INVALID_CONTRACT_ID);
        final ContractID contractId = (ContractID) op.balanceSource().value();
        validateTruePreCheck(contractId.shardNum() == 0, INVALID_CONTRACT_ID);
        validateTruePreCheck(contractId.realmNum() == 0, INVALID_CONTRACT_ID);
        validateTruePreCheck(
                (contractId.hasContractNum() && contractId.contractNumOrThrow() >= 0) || contractId.hasEvmAddress(),
                INVALID_CONTRACT_ID);
        final var contract = accountStore.getContractById(requireNonNull(op.contractID()));
        validateFalsePreCheck(contract == null, INVALID_CONTRACT_ID);
        validateTruePreCheck(contract.smartContract(), INVALID_CONTRACT_ID);
        validateFalsePreCheck(contract.deleted(), CONTRACT_DELETED);
    }

    private void validateAccountId(CryptoGetAccountBalanceQuery op, ReadableAccountStore accountStore)
            throws PreCheckException {
        AccountID accountId = (AccountID) op.balanceSource().value();
        validateTruePreCheck(accountId.shardNum() == 0, INVALID_ACCOUNT_ID);
        validateTruePreCheck(accountId.realmNum() == 0, INVALID_ACCOUNT_ID);
        validateAccountID(accountId, INVALID_ACCOUNT_ID);
        final var account = accountStore.getAliasedAccountById(requireNonNull(op.accountID()));
        validateFalsePreCheck(account == null, INVALID_ACCOUNT_ID);
        validateFalsePreCheck(account.deleted(), ACCOUNT_DELETED);
    }

    @Override
    public Response findResponse(@NonNull final QueryContext context, @NonNull final ResponseHeader header) {
        requireNonNull(context);
        requireNonNull(header);
        final var query = context.query();
        final var config = context.configuration().getConfigData(TokensConfig.class);
        final var accountStore = context.createStore(ReadableAccountStore.class);
        final var tokenRelationStore = context.createStore(ReadableTokenRelationStore.class);
        final var tokenStore = context.createStore(ReadableTokenStore.class);
        final var op = query.cryptogetAccountBalanceOrThrow();
        final var response = CryptoGetAccountBalanceResponse.newBuilder();

        response.header(header);
        if (header.nodeTransactionPrecheckCode() == OK) {
            final var account = op.hasAccountID()
                    ? accountStore.getAliasedAccountById(op.accountIDOrThrow())
                    : accountStore.getContractById(op.contractIDOrThrow());
            requireNonNull(account);
            response.accountID(account.accountIdOrThrow()).balance(account.tinybarBalance());
            if (config.balancesInQueriesEnabled()) {
                final var tokenBalances = getTokenBalances(config, account, tokenStore, tokenRelationStore);
                balanceSpeedometer.update(tokenBalances.size());
                response.tokenBalances(tokenBalances);
            }
        }

        return Response.newBuilder().cryptogetAccountBalance(response).build();
    }

    /**
     * Calculate TokenBalance of an Account
     *
     * @param tokenConfig use TokenConfig to get maxRelsPerInfoQuery value
     * @param account the account to be calculated from
     * @param readableTokenStore readable token store
     * @param tokenRelationStore token relation store
     * @return ArrayList of TokenBalance object
     */
    private List getTokenBalances(
            @NonNull final TokensConfig tokenConfig,
            @NonNull final Account account,
            @NonNull final ReadableTokenStore readableTokenStore,
            @NonNull final ReadableTokenRelationStore tokenRelationStore) {
        final var ret = new ArrayList();
        var tokenId = account.headTokenId();
        int count = 0;
        TokenRelation tokenRelation;
        Token token; // token from readableToken store by tokenID
        AccountID accountID; // build from accountNumber
        TokenBalance tokenBalance; // created TokenBalance object
        while (tokenId != null && !tokenId.equals(TokenID.DEFAULT) && count < tokenConfig.maxRelsPerInfoQuery()) {
            accountID = account.accountId();
            tokenRelation = tokenRelationStore.get(accountID, tokenId);
            if (tokenRelation != null) {
                token = readableTokenStore.get(tokenId);
                if (token != null) {
                    tokenBalance = TokenBalance.newBuilder()
                            .tokenId(tokenId)
                            .balance(tokenRelation.balance())
                            .decimals(token.decimals())
                            .build();
                    ret.add(tokenBalance);
                }
                tokenId = tokenRelation.nextToken();
            } else {
                break;
            }
            count++;
        }
        return ret;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy