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

com.hedera.node.app.service.token.impl.handlers.CryptoTransferHandler 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.CUSTOM_FEE_CHARGING_EXCEEDED_MAX_ACCOUNT_AMOUNTS;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE_FOR_CUSTOM_FEE;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSFER_ACCOUNT_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TREASURY_ACCOUNT_FOR_TOKEN;
import static com.hedera.hapi.node.base.ResponseCodeEnum.NOT_SUPPORTED;
import static com.hedera.hapi.node.base.SubType.DEFAULT;
import static com.hedera.hapi.node.base.SubType.TOKEN_FUNGIBLE_COMMON;
import static com.hedera.hapi.node.base.SubType.TOKEN_FUNGIBLE_COMMON_WITH_CUSTOM_FEES;
import static com.hedera.hapi.node.base.SubType.TOKEN_NON_FUNGIBLE_UNIQUE;
import static com.hedera.hapi.node.base.SubType.TOKEN_NON_FUNGIBLE_UNIQUE_WITH_CUSTOM_FEES;
import static com.hedera.hapi.util.HapiUtils.isHollow;
import static com.hedera.node.app.hapi.fees.usage.SingletonUsageProperties.USAGE_PROPERTIES;
import static com.hedera.node.app.hapi.fees.usage.crypto.CryptoOpsUsage.LONG_ACCOUNT_AMOUNT_BYTES;
import static com.hedera.node.app.hapi.fees.usage.token.TokenOpsUsage.LONG_BASIC_ENTITY_ID_SIZE;
import static com.hedera.node.app.hapi.fees.usage.token.entities.TokenEntitySizes.TOKEN_ENTITY_SIZES;
import static com.hedera.node.app.service.token.AliasUtils.isAlias;
import static com.hedera.node.app.service.token.impl.handlers.BaseCryptoHandler.isStakingAccount;
import static com.hedera.node.app.spi.key.KeyUtils.isValid;
import static com.hedera.node.app.spi.validation.Validations.validateAccountID;
import static com.hedera.node.app.spi.workflows.HandleException.validateTrue;
import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck;
import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.base.AccountAmount;
import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.HederaFunctionality;
import com.hedera.hapi.node.base.NftTransfer;
import com.hedera.hapi.node.base.SubType;
import com.hedera.hapi.node.base.TokenID;
import com.hedera.hapi.node.base.TokenTransferList;
import com.hedera.hapi.node.base.TransferList;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.hapi.node.state.token.Nft;
import com.hedera.hapi.node.state.token.Token;
import com.hedera.hapi.node.token.CryptoTransferTransactionBody;
import com.hedera.hapi.node.transaction.AssessedCustomFee;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.service.token.ReadableAccountStore;
import com.hedera.node.app.service.token.ReadableNftStore;
import com.hedera.node.app.service.token.ReadableTokenRelationStore;
import com.hedera.node.app.service.token.ReadableTokenStore;
import com.hedera.node.app.service.token.ReadableTokenStore.TokenMetadata;
import com.hedera.node.app.service.token.impl.handlers.transfer.AdjustFungibleTokenChangesStep;
import com.hedera.node.app.service.token.impl.handlers.transfer.AdjustHbarChangesStep;
import com.hedera.node.app.service.token.impl.handlers.transfer.AssociateTokenRecipientsStep;
import com.hedera.node.app.service.token.impl.handlers.transfer.CustomFeeAssessmentStep;
import com.hedera.node.app.service.token.impl.handlers.transfer.EnsureAliasesStep;
import com.hedera.node.app.service.token.impl.handlers.transfer.NFTOwnersChangeStep;
import com.hedera.node.app.service.token.impl.handlers.transfer.ReplaceAliasesWithIDsInOp;
import com.hedera.node.app.service.token.impl.handlers.transfer.TransferContextImpl;
import com.hedera.node.app.service.token.impl.handlers.transfer.TransferStep;
import com.hedera.node.app.service.token.impl.validators.CryptoTransferValidator;
import com.hedera.node.app.service.token.records.CryptoTransferRecordBuilder;
import com.hedera.node.app.spi.fees.FeeContext;
import com.hedera.node.app.spi.fees.Fees;
import com.hedera.node.app.spi.workflows.HandleContext;
import com.hedera.node.app.spi.workflows.HandleException;
import com.hedera.node.app.spi.workflows.PreCheckException;
import com.hedera.node.app.spi.workflows.PreHandleContext;
import com.hedera.node.app.spi.workflows.TransactionHandler;
import com.hedera.node.app.spi.workflows.WarmupContext;
import com.hedera.node.config.data.FeesConfig;
import com.hedera.node.config.data.HederaConfig;
import com.hedera.node.config.data.LazyCreationConfig;
import com.hedera.node.config.data.LedgerConfig;
import com.hedera.node.config.data.TokensConfig;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * This class contains all workflow-related functionality regarding {@link
 * HederaFunctionality#CRYPTO_TRANSFER}.
 */
@Singleton
public class CryptoTransferHandler implements TransactionHandler {
    private final CryptoTransferValidator validator;
    private final boolean enforceMonoServiceRestrictionsOnAutoCreationCustomFeePayments;

    /**
     * Default constructor for injection.
     * @param validator the validator to use to validate the transaction
     */
    @Inject
    public CryptoTransferHandler(@NonNull final CryptoTransferValidator validator) {
        this(validator, true);
    }

    /**
     * Constructor for injection with the option to enforce mono-service restrictions on auto-creation custom fee
     * @param validator the validator to use to validate the transaction
     * @param enforceMonoServiceRestrictionsOnAutoCreationCustomFeePayments whether to enforce mono-service restrictions
     */
    public CryptoTransferHandler(
            @NonNull final CryptoTransferValidator validator,
            final boolean enforceMonoServiceRestrictionsOnAutoCreationCustomFeePayments) {
        this.validator = validator;
        this.enforceMonoServiceRestrictionsOnAutoCreationCustomFeePayments =
                enforceMonoServiceRestrictionsOnAutoCreationCustomFeePayments;
    }

    @Override
    public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException {
        requireNonNull(context);
        pureChecks(context.body());

        final var op = context.body().cryptoTransferOrThrow();
        final var accountStore = context.createStore(ReadableAccountStore.class);
        final var tokenStore = context.createStore(ReadableTokenStore.class);
        for (final var transfers : op.tokenTransfers()) {
            final var tokenMeta = tokenStore.getTokenMeta(transfers.tokenOrElse(TokenID.DEFAULT));
            if (tokenMeta == null) throw new PreCheckException(INVALID_TOKEN_ID);
            checkFungibleTokenTransfers(transfers.transfers(), context, accountStore, false);
            checkNftTransfers(transfers.nftTransfers(), context, tokenMeta, op, accountStore);
        }

        final var hbarTransfers = op.transfersOrElse(TransferList.DEFAULT).accountAmounts();
        checkFungibleTokenTransfers(hbarTransfers, context, accountStore, true);
    }

    @Override
    public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException {
        requireNonNull(txn);
        final var op = txn.cryptoTransfer();
        validateTruePreCheck(op != null, INVALID_TRANSACTION_BODY);
        validator.pureChecks(op);
    }

    @Override
    public void warm(@NonNull final WarmupContext context) {
        requireNonNull(context);

        final ReadableAccountStore accountStore = context.createStore(ReadableAccountStore.class);
        final ReadableTokenStore tokenStore = context.createStore(ReadableTokenStore.class);
        final ReadableNftStore nftStore = context.createStore(ReadableNftStore.class);
        final ReadableTokenRelationStore tokenRelationStore = context.createStore(ReadableTokenRelationStore.class);
        final CryptoTransferTransactionBody op = context.body().cryptoTransferOrThrow();

        // warm all accounts from the transfer list
        final TransferList transferList = op.transfersOrElse(TransferList.DEFAULT);
        transferList.accountAmounts().stream()
                .map(AccountAmount::accountID)
                .filter(Objects::nonNull)
                .forEach(accountStore::warm);

        // warm all token-data from the token transfer list
        final List tokenTransfers = op.tokenTransfers();
        tokenTransfers.stream().filter(TokenTransferList::hasToken).forEach(tokenTransferList -> {
            final TokenID tokenID = tokenTransferList.tokenOrThrow();
            final Token token = tokenStore.get(tokenID);
            final AccountID treasuryID = token == null ? null : token.treasuryAccountId();
            if (treasuryID != null) {
                accountStore.warm(treasuryID);
            }
            for (final AccountAmount amount : tokenTransferList.transfers()) {
                amount.ifAccountID(accountID -> tokenRelationStore.warm(accountID, tokenID));
            }
            for (final NftTransfer nftTransfer : tokenTransferList.nftTransfers()) {
                warmNftTransfer(accountStore, tokenStore, nftStore, tokenRelationStore, tokenID, nftTransfer);
            }
        });
    }

    private void warmNftTransfer(
            @NonNull final ReadableAccountStore accountStore,
            @NonNull final ReadableTokenStore tokenStore,
            @NonNull final ReadableNftStore nftStore,
            @NonNull final ReadableTokenRelationStore tokenRelationStore,
            @NonNull final TokenID tokenID,
            @NonNull final NftTransfer nftTransfer) {
        // warm sender
        nftTransfer.ifSenderAccountID(senderAccountID -> {
            final Account sender = accountStore.getAliasedAccountById(senderAccountID);
            if (sender != null) {
                sender.ifHeadNftId(nftStore::warm);
            }
            tokenRelationStore.warm(senderAccountID, tokenID);
        });

        // warm receiver
        nftTransfer.ifReceiverAccountID(receiverAccountID -> {
            final Account receiver = accountStore.getAliasedAccountById(receiverAccountID);
            if (receiver != null) {
                receiver.ifHeadTokenId(headTokenID -> {
                    tokenRelationStore.warm(receiverAccountID, headTokenID);
                    tokenStore.warm(headTokenID);
                });
                receiver.ifHeadNftId(nftStore::warm);
            }
            tokenRelationStore.warm(receiverAccountID, tokenID);
        });

        // warm neighboring NFTs
        final Nft nft = nftStore.get(tokenID, nftTransfer.serialNumber());
        if (nft != null) {
            nft.ifOwnerPreviousNftId(nftStore::warm);
            nft.ifOwnerNextNftId(nftStore::warm);
        }
    }

    @Override
    public void handle(@NonNull final HandleContext context) throws HandleException {
        requireNonNull(context);
        final var txn = context.body();
        final var op = txn.cryptoTransferOrThrow();
        final var topLevelPayer = context.payer();

        final var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class);
        final var hederaConfig = context.configuration().getConfigData(HederaConfig.class);
        final var tokensConfig = context.configuration().getConfigData(TokensConfig.class);

        validator.validateSemantics(op, ledgerConfig, hederaConfig, tokensConfig);

        // create a new transfer context that is specific only for this transaction
        final var transferContext =
                new TransferContextImpl(context, enforceMonoServiceRestrictionsOnAutoCreationCustomFeePayments);

        transferContext.validateHbarAllowances();

        // Replace all aliases in the transaction body with its account ids
        final var replacedOp = ensureAndReplaceAliasesInOp(txn, transferContext, context);
        // Use the op with replaced aliases in further steps
        final var steps = decomposeIntoSteps(replacedOp, topLevelPayer, transferContext);
        for (final var step : steps) {
            // Apply all changes to the handleContext's States
            step.doIn(transferContext);
        }

        final var recordBuilder = context.recordBuilder(CryptoTransferRecordBuilder.class);
        if (!transferContext.getAutomaticAssociations().isEmpty()) {
            transferContext.getAutomaticAssociations().forEach(recordBuilder::addAutomaticTokenAssociation);
        }
        if (!transferContext.getAssessedCustomFees().isEmpty()) {
            recordBuilder.assessedCustomFees(transferContext.getAssessedCustomFees());
        }
    }

    /**
     * Ensures all aliases specified in the transfer exist. If the aliases are in receiver section, and don't exist
     * they will be auto-created. This step populates resolved aliases and number of auto creations in the
     * transferContext, which is used by subsequent steps and throttling.
     * It will also replace all aliases in the {@link CryptoTransferTransactionBody} with its account ids, so it will
     * be easier to process in next steps.
     * @param txn the given transaction body
     * @param transferContext the given transfer context
     * @param context the given handle context
     * @return the replaced transaction body with all aliases replaced with its account ids
     * @throws HandleException if any error occurs during the process
     */
    private CryptoTransferTransactionBody ensureAndReplaceAliasesInOp(
            @NonNull final TransactionBody txn,
            @NonNull final TransferContextImpl transferContext,
            @NonNull final HandleContext context)
            throws HandleException {
        final var op = txn.cryptoTransferOrThrow();

        // ensure all aliases exist, if not create then if receivers
        ensureExistenceOfAliasesOrCreate(op, transferContext);
        if (transferContext.numOfLazyCreations() > 0) {
            final var config = context.configuration().getConfigData(LazyCreationConfig.class);
            validateTrue(config.enabled(), NOT_SUPPORTED);
        }

        // replace all aliases with its account ids, so it will be easier to process in next steps
        final var replacedOp = new ReplaceAliasesWithIDsInOp().replaceAliasesWithIds(op, transferContext);
        // re-run pure checks on this op to see if there are no duplicates
        try {
            final var txnBody = txn.copyBuilder().cryptoTransfer(replacedOp).build();
            pureChecks(txnBody);
        } catch (PreCheckException e) {
            throw new HandleException(e.responseCode());
        }
        return replacedOp;
    }

    private void ensureExistenceOfAliasesOrCreate(
            @NonNull final CryptoTransferTransactionBody op, @NonNull final TransferContextImpl transferContext) {
        final var ensureAliasExistence = new EnsureAliasesStep(op);
        ensureAliasExistence.doIn(transferContext);
    }

    /**
     * Decomposes a crypto transfer into a sequence of steps that can be executed in order.
     * Each step validates the preconditions needed from TransferContextImpl in order to perform its action.
     * Steps are as follows:
     * 
    *
  1. (c,o)Ensure existence of alias-referenced accounts
  2. *
  3. (+,c)Charge custom fees for token transfers
  4. *
  5. (o)Ensure associations of token recipients
  6. *
  7. (+)Do zero-sum hbar balance changes
  8. *
  9. (+)Do zero-sum fungible token transfers
  10. *
  11. (+)Change NFT owners
  12. *
  13. (+,c)Pay staking rewards, possibly to previously unmentioned stakee accounts
  14. *
* LEGEND: '+' = creates new BalanceChange(s) from either the transaction body, custom fee schedule, or staking reward situation * 'c' = updates an existing BalanceChange * 'o' = causes a side effect not represented as BalanceChange * * @param op The crypto transfer transaction body * @param topLevelPayer The payer of the transaction * @param transferContext * @return A list of steps to execute */ private List decomposeIntoSteps( final CryptoTransferTransactionBody op, final AccountID topLevelPayer, final TransferContextImpl transferContext) { final List steps = new ArrayList<>(); // Step 1: associate any token recipients that are not already associated and have // auto association slots open steps.add(new AssociateTokenRecipientsStep(op)); // Step 2: Charge custom fees for token transfers final var customFeeStep = new CustomFeeAssessmentStep(op); // The below steps should be doe for both custom fee assessed transaction in addition to // original transaction final var customFeeAssessedOps = customFeeStep.assessCustomFees(transferContext); for (final var txn : customFeeAssessedOps) { steps.add(new AssociateTokenRecipientsStep(txn)); // Step 3: Charge hbar transfers and also ones with isApproval. Modify the allowances map on account final var assessHbarTransfers = new AdjustHbarChangesStep(txn, topLevelPayer); steps.add(assessHbarTransfers); // Step 4: Charge token transfers with an approval. Modify the allowances map on account final var assessFungibleTokenTransfers = new AdjustFungibleTokenChangesStep(txn, topLevelPayer); steps.add(assessFungibleTokenTransfers); // Step 5: Change NFT owners and also ones with isApproval. Clear the spender on NFT. // Will be a no-op for every txn except possibly the first (i.e., the top-level txn). // This is because assessed custom fees never change NFT owners final var changeNftOwners = new NFTOwnersChangeStep(txn, topLevelPayer); steps.add(changeNftOwners); } return steps; } /** * As part of pre-handle, checks that HBAR or fungible token transfers in the transfer list are plausible. * * @param transfers The transfers to check * @param ctx The context we gather signing keys into * @param accountStore The account store to use to look up accounts * @param hbarTransfer Whether this is a hbar transfer. When HIP-583 is implemented, we can remove this argument. * @throws PreCheckException If the transaction is invalid */ private void checkFungibleTokenTransfers( @NonNull final List transfers, @NonNull final PreHandleContext ctx, @NonNull final ReadableAccountStore accountStore, final boolean hbarTransfer) throws PreCheckException { // We're going to iterate over all the transfers in the transfer list. Each transfer is known as an // "account amount". Each of these represents the transfer of hbar INTO a single account or OUT of a // single account. for (final var accountAmount : transfers) { // Given an accountId, we need to look up the associated account. final var accountId = validateAccountID(accountAmount.accountIDOrElse(AccountID.DEFAULT), null); final var account = accountStore.getAliasedAccountById(accountId); final var isCredit = accountAmount.amount() > 0; final var isDebit = accountAmount.amount() < 0; if (account != null) { // This next code is not right, but we have it for compatibility until after we migrate // off the mono-service. Then we can fix this. In this logic, if the receiver account (the // one with the credit) doesn't have a key AND the value being sent is non-hbar fungible tokens, // then we fail with ACCOUNT_IS_IMMUTABLE. And if the account is being debited and has no key, // then we also fail with the same error. It should be that being credited value DOES NOT require // a key, unless `receiverSigRequired` is true. if (isStakingAccount(ctx.configuration(), account.accountId()) && (isDebit || (isCredit && !hbarTransfer))) { // NOTE: should change to ACCOUNT_IS_IMMUTABLE after modularization throw new PreCheckException(INVALID_ACCOUNT_ID); } // We only need signing keys for accounts that are being debited OR those being credited // but with receiverSigRequired set to true. If the account is being debited but "isApproval" // is set on the transaction, then we defer to the token transfer logic to determine if all // signing requirements were met ("isApproval" is a way for the client to say "I don't need a key // because I'm approved which you will see when you handle this transaction"). if (isDebit && !accountAmount.isApproval()) { // If the account is a hollow account, then we require a signature for it. // It is possible that the hollow account has signed this transaction, in which case // we need to finalize the hollow account by setting its key. if (isHollow(account)) { ctx.requireSignatureForHollowAccount(account); } else { ctx.requireKeyOrThrow(account.key(), INVALID_ACCOUNT_ID); } } else if (isCredit && account.receiverSigRequired()) { ctx.requireKeyOrThrow(account.key(), INVALID_TRANSFER_ACCOUNT_ID); } } else if (hbarTransfer) { // It is possible for the transfer to be valid even if the account is not found. For example, we // allow auto-creation of "hollow accounts" if you transfer value into an account *by alias* that // didn't previously exist. If that is not the case, then we fail because we couldn't find the // destination account. if (!isCredit || !isAlias(accountId)) { // Interestingly, this means that if the transfer amount is exactly 0 and the account has a // non-existent alias, then we fail. throw new PreCheckException(INVALID_ACCOUNT_ID); } } else if (isDebit) { // All debited accounts must be valid throw new PreCheckException(INVALID_ACCOUNT_ID); } } } private void checkNftTransfers( final List nftTransfersList, final PreHandleContext meta, final TokenMetadata tokenMeta, final CryptoTransferTransactionBody op, final ReadableAccountStore accountStore) throws PreCheckException { for (final var nftTransfer : nftTransfersList) { final var senderId = nftTransfer.senderAccountIDOrElse(AccountID.DEFAULT); validateAccountID(senderId, null); checkSender(senderId, nftTransfer, meta, accountStore); final var receiverId = nftTransfer.receiverAccountIDOrElse(AccountID.DEFAULT); validateAccountID(receiverId, null); checkReceiver(receiverId, senderId, nftTransfer, meta, tokenMeta, op, accountStore); } } private void checkReceiver( final AccountID receiverId, final AccountID senderId, final NftTransfer nftTransfer, final PreHandleContext meta, final TokenMetadata tokenMeta, final CryptoTransferTransactionBody op, final ReadableAccountStore accountStore) throws PreCheckException { // Lookup the receiver account and verify it. final var receiverAccount = accountStore.getAliasedAccountById(receiverId); if (receiverAccount == null) { // It may be that the receiver account does not yet exist. If it is being addressed by alias, // then this is OK, as we will automatically create the account. Otherwise, fail. if (!isAlias(receiverId)) { throw new PreCheckException(INVALID_ACCOUNT_ID); } else { return; } } final var receiverKey = receiverAccount.key(); if (isStakingAccount(meta.configuration(), receiverAccount.accountId())) { // If the receiver account has no key, then fail with INVALID_ACCOUNT_ID. // NOTE: should change to ACCOUNT_IS_IMMUTABLE after modularization throw new PreCheckException(INVALID_ACCOUNT_ID); } else if (receiverAccount.receiverSigRequired()) { // If receiverSigRequired is set, and if there is no key on the receiver's account, then fail with // INVALID_TRANSFER_ACCOUNT_ID. Otherwise, add the key. meta.requireKeyOrThrow(receiverKey, INVALID_TRANSFER_ACCOUNT_ID); } else if (tokenMeta.hasRoyaltyWithFallback() && !receivesFungibleValue(nftTransfer.senderAccountID(), op, accountStore)) { // It may be that this transfer has royalty fees associated with it. If it does, then we need // to check that the receiver signed the transaction, UNLESS the sender or receiver is // the treasury, in which case fallback fees will not be applied when the transaction is handled, // so the receiver key does not need to sign. final var treasuryId = tokenMeta.treasuryAccountId(); if (!treasuryId.equals(senderId) && !treasuryId.equals(receiverId)) { meta.requireKeyOrThrow(receiverId, INVALID_TREASURY_ACCOUNT_FOR_TOKEN); } } } private void checkSender( final AccountID senderId, final NftTransfer nftTransfer, final PreHandleContext meta, final ReadableAccountStore accountStore) throws PreCheckException { // Lookup the sender account and verify it. final var senderAccount = accountStore.getAliasedAccountById(senderId); if (senderAccount == null) { throw new PreCheckException(INVALID_ACCOUNT_ID); } // If the sender account is immutable, then we throw an exception. final var key = senderAccount.key(); if (key == null || !isValid(key)) { if (isHollow(senderAccount)) { meta.requireSignatureForHollowAccount(senderAccount); } else { // If the sender account has no key, then fail with INVALID_ACCOUNT_ID. // NOTE: should change to ACCOUNT_IS_IMMUTABLE throw new PreCheckException(INVALID_ACCOUNT_ID); } } else if (!nftTransfer.isApproval()) { meta.requireKey(key); } } private boolean receivesFungibleValue( final AccountID target, final CryptoTransferTransactionBody op, final ReadableAccountStore accountStore) { for (final var adjust : op.transfersOrElse(TransferList.DEFAULT).accountAmounts()) { final var unaliasedAccount = accountStore.getAliasedAccountById(adjust.accountIDOrElse(AccountID.DEFAULT)); final var unaliasedTarget = accountStore.getAliasedAccountById(target); if (unaliasedAccount != null && unaliasedTarget != null && adjust.amount() > 0 && unaliasedAccount.equals(unaliasedTarget)) { return true; } } for (final var transfers : op.tokenTransfers()) { for (final var adjust : transfers.transfers()) { final var unaliasedAccount = accountStore.getAliasedAccountById(adjust.accountIDOrElse(AccountID.DEFAULT)); final var unaliasedTarget = accountStore.getAliasedAccountById(target); if (unaliasedAccount != null && unaliasedTarget != null && adjust.amount() > 0 && unaliasedAccount.equals(unaliasedTarget)) { return true; } } } return false; } @NonNull @Override public Fees calculateFees(@NonNull final FeeContext feeContext) { final var body = feeContext.body(); final var op = body.cryptoTransferOrThrow(); final var config = feeContext.configuration(); final var tokenMultiplier = config.getConfigData(FeesConfig.class).tokenTransferUsageMultiplier(); /* BPT calculations shouldn't include any custom fee payment usage */ int totalXfers = op.transfersOrElse(TransferList.DEFAULT).accountAmounts().size(); var totalTokensInvolved = 0; var totalTokenTransfers = 0; var numNftOwnershipChanges = 0; for (final var tokenTransfers : op.tokenTransfers()) { totalTokensInvolved++; totalTokenTransfers += tokenTransfers.transfers().size(); numNftOwnershipChanges += tokenTransfers.nftTransfers().size(); } int weightedTokensInvolved = tokenMultiplier * totalTokensInvolved; int weightedTokenXfers = tokenMultiplier * totalTokenTransfers; final var bpt = weightedTokensInvolved * LONG_BASIC_ENTITY_ID_SIZE + (weightedTokenXfers + totalXfers) * LONG_ACCOUNT_AMOUNT_BYTES + TOKEN_ENTITY_SIZES.bytesUsedForUniqueTokenTransfers(numNftOwnershipChanges); /* Include custom fee payment usage in RBS calculations */ var customFeeHbarTransfers = 0; var customFeeTokenTransfers = 0; final var involvedTokens = new HashSet(); final var customFeeAssessor = new CustomFeeAssessmentStep(op); List assessedCustomFees; boolean triedAndFailedToUseCustomFees = false; try { assessedCustomFees = customFeeAssessor.assessNumberOfCustomFees(feeContext); } catch (HandleException ignore) { final var status = ignore.getStatus(); // If the transaction tried and failed to use custom fees, enable this flag. // This is used to charge a different canonical fees. triedAndFailedToUseCustomFees = status == INSUFFICIENT_PAYER_BALANCE_FOR_CUSTOM_FEE || status == INSUFFICIENT_SENDER_ACCOUNT_BALANCE_FOR_CUSTOM_FEE || status == CUSTOM_FEE_CHARGING_EXCEEDED_MAX_ACCOUNT_AMOUNTS; assessedCustomFees = new ArrayList<>(); } for (final var fee : assessedCustomFees) { if (!fee.hasTokenId()) { customFeeHbarTransfers++; } else { customFeeTokenTransfers++; involvedTokens.add(fee.tokenId()); } } totalXfers += customFeeHbarTransfers; weightedTokenXfers += tokenMultiplier * customFeeTokenTransfers; weightedTokensInvolved += tokenMultiplier * involvedTokens.size(); long rbs = (totalXfers * LONG_ACCOUNT_AMOUNT_BYTES) + TOKEN_ENTITY_SIZES.bytesUsedToRecordTokenTransfers( weightedTokensInvolved, weightedTokenXfers, numNftOwnershipChanges); /* Get subType based on the above information */ final var subType = getSubType( numNftOwnershipChanges, totalTokenTransfers, customFeeHbarTransfers, customFeeTokenTransfers, triedAndFailedToUseCustomFees); return feeContext .feeCalculator(subType) .addBytesPerTransaction(bpt) .addRamByteSeconds(rbs * USAGE_PROPERTIES.legacyReceiptStorageSecs()) .calculate(); } /** * Get the subType based on the number of NFT ownership changes, number of fungible token transfers, * number of custom fee hbar transfers, number of custom fee token transfers and whether the transaction * tried and failed to use custom fees. * @param numNftOwnershipChanges number of NFT ownership changes * @param numFungibleTokenTransfers number of fungible token transfers * @param customFeeHbarTransfers number of custom fee hbar transfers * @param customFeeTokenTransfers number of custom fee token transfers * @param triedAndFailedToUseCustomFees whether the transaction tried and failed while validating custom fees. * If the failure includes custom fee error codes, the fee charged should not * use SubType.DEFAULT. * @return the subType */ private SubType getSubType( final int numNftOwnershipChanges, final int numFungibleTokenTransfers, final int customFeeHbarTransfers, final int customFeeTokenTransfers, final boolean triedAndFailedToUseCustomFees) { if (triedAndFailedToUseCustomFees) { return TOKEN_FUNGIBLE_COMMON_WITH_CUSTOM_FEES; } if (numNftOwnershipChanges != 0) { if (customFeeHbarTransfers > 0 || customFeeTokenTransfers > 0) { return TOKEN_NON_FUNGIBLE_UNIQUE_WITH_CUSTOM_FEES; } return TOKEN_NON_FUNGIBLE_UNIQUE; } if (numFungibleTokenTransfers != 0) { if (customFeeHbarTransfers > 0 || customFeeTokenTransfers > 0) { return TOKEN_FUNGIBLE_COMMON_WITH_CUSTOM_FEES; } return TOKEN_FUNGIBLE_COMMON; } return DEFAULT; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy