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

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

/*
 * Copyright (C) 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.EMPTY_PENDING_AIRDROP_ID_LIST;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_NFT_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_NFT_SERIAL_NUMBER;
import static com.hedera.hapi.node.base.ResponseCodeEnum.NOT_SUPPORTED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.PENDING_AIRDROP_ID_LIST_TOO_LONG;
import static com.hedera.hapi.node.base.ResponseCodeEnum.PENDING_AIRDROP_ID_REPEATED;
import static com.hedera.node.app.service.token.impl.util.AirdropHandlerHelper.IdType.SENDER;
import static com.hedera.node.app.service.token.impl.util.AirdropHandlerHelper.standardizeAirdropIds;
import static com.hedera.node.app.spi.validation.Validations.validateAccountID;
import static com.hedera.node.app.spi.workflows.HandleException.validateFalse;
import static com.hedera.node.app.spi.workflows.HandleException.validateTrue;
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.HederaFunctionality;
import com.hedera.hapi.node.base.PendingAirdropId;
import com.hedera.hapi.node.base.SubType;
import com.hedera.hapi.node.base.TokenID;
import com.hedera.hapi.node.token.TokenCancelAirdropTransactionBody;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.service.token.impl.WritableAccountStore;
import com.hedera.node.app.service.token.impl.WritableAirdropStore;
import com.hedera.node.app.service.token.impl.util.PendingAirdropUpdater;
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.config.data.TokensConfig;
import com.swirlds.config.api.Configuration;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.EnumSet;
import java.util.HashSet;
import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * This class contains all workflow-related functionality regarding {@link HederaFunctionality#TOKEN_CANCEL_AIRDROP}.
 * This transaction type is used to cancel an airdrop which is in pending state.
 */
@Singleton
public class TokenCancelAirdropHandler extends BaseTokenHandler implements TransactionHandler {
    private final PendingAirdropUpdater pendingAirdropUpdater;

    @Inject
    public TokenCancelAirdropHandler(@NonNull final PendingAirdropUpdater pendingAirdropUpdater) {
        // Exists for injection
        this.pendingAirdropUpdater = requireNonNull(pendingAirdropUpdater);
    }

    @Override
    public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException {
        requireNonNull(context);
        final var txn = context.body();
        final var op = txn.tokenCancelAirdropOrThrow();
        final var allPendingAirdrops = op.pendingAirdrops();
        for (final var airdrop : allPendingAirdrops) {
            context.requireAliasedKeyOrThrow(airdrop.senderIdOrThrow(), INVALID_ACCOUNT_ID);
        }
    }

    @Override
    public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException {
        requireNonNull(txn, "Transaction body cannot be null");
        final var op = txn.tokenCancelAirdropOrThrow();

        validateFalsePreCheck(op.pendingAirdrops().isEmpty(), EMPTY_PENDING_AIRDROP_ID_LIST);
        final var uniquePendingAirdrops = new HashSet();
        for (final var airdrop : op.pendingAirdrops()) {
            if (!uniquePendingAirdrops.add(airdrop)) {
                throw new PreCheckException(PENDING_AIRDROP_ID_REPEATED);
            }
            validateAccountID(airdrop.receiverId(), null);
            validateAccountID(airdrop.senderId(), null);

            if (airdrop.hasFungibleTokenType()) {
                final var tokenID = airdrop.fungibleTokenType();
                validateTruePreCheck(tokenID != null && !tokenID.equals(TokenID.DEFAULT), INVALID_TOKEN_ID);
            }
            if (airdrop.hasNonFungibleToken()) {
                final var nftID = airdrop.nonFungibleTokenOrThrow();
                validateTruePreCheck(nftID.tokenId() != null, INVALID_NFT_ID);
                validateTruePreCheck(nftID.serialNumber() > 0, INVALID_TOKEN_NFT_SERIAL_NUMBER);
            }
        }
    }

    @SuppressWarnings("java:S3864")
    @Override
    public void handle(@NonNull final HandleContext context) throws HandleException {
        requireNonNull(context);
        final var txn = context.body();
        final var op = txn.tokenCancelAirdropOrThrow();
        configValidation(context.configuration(), op);

        final var accountStore = context.storeFactory().writableStore(WritableAccountStore.class);
        final var airdropStore = context.storeFactory().writableStore(WritableAirdropStore.class);

        final var standardAirdropIds =
                standardizeAirdropIds(accountStore, airdropStore, op.pendingAirdrops(), EnumSet.of(SENDER));
        pendingAirdropUpdater.removePendingAirdrops(standardAirdropIds, airdropStore, accountStore);
    }

    /**
     * Using the configuration to validate if the body valid
     */
    private void configValidation(
            @NonNull final Configuration configuration, @NonNull final TokenCancelAirdropTransactionBody op) {
        requireNonNull(configuration);
        requireNonNull(op);

        var tokensConfig = configuration.getConfigData(TokensConfig.class);
        validateTrue(tokensConfig.cancelTokenAirdropEnabled(), NOT_SUPPORTED);
        validateFalse(
                op.pendingAirdrops().size() > tokensConfig.maxAllowedPendingAirdropsToCancel(),
                PENDING_AIRDROP_ID_LIST_TOO_LONG);
    }

    @NonNull
    @Override
    public Fees calculateFees(@NonNull final FeeContext feeContext) {
        var tokensConfig = feeContext.configuration().getConfigData(TokensConfig.class);
        validateTrue(tokensConfig.cancelTokenAirdropEnabled(), NOT_SUPPORTED);
        final var feeCalculator = feeContext.feeCalculatorFactory().feeCalculator(SubType.DEFAULT);
        feeCalculator.resetUsage();

        return feeCalculator
                .addVerificationsPerTransaction(Math.max(0, feeContext.numTxnSignatures() - 1))
                .calculate();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy