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

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

There is a newer version: 0.57.3
Show newest version
/*
 * 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.PENDING_AIRDROP_ID_LIST_TOO_LONG;
import static com.hedera.hapi.node.base.ResponseCodeEnum.PENDING_AIRDROP_ID_REPEATED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_AIRDROP_WITH_FALLBACK_ROYALTY;
import static com.hedera.node.app.service.token.impl.util.TokenHandlerHelper.getIfUsable;
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.PendingAirdropId;
import com.hedera.hapi.node.base.ResponseCodeEnum;
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.state.token.Token;
import com.hedera.hapi.node.token.CryptoTransferTransactionBody;
import com.hedera.hapi.node.token.TokenClaimAirdropTransactionBody;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.service.token.ReadableAccountStore;
import com.hedera.node.app.service.token.ReadableAirdropStore;
import com.hedera.node.app.service.token.ReadableTokenStore;
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.WritableTokenRelationStore;
import com.hedera.node.app.service.token.impl.handlers.transfer.TransferContextImpl;
import com.hedera.node.app.service.token.impl.handlers.transfer.TransferExecutor;
import com.hedera.node.app.service.token.impl.util.AirdropHandlerHelper;
import com.hedera.node.app.service.token.impl.util.PendingAirdropUpdater;
import com.hedera.node.app.service.token.impl.validators.CryptoTransferValidator;
import com.hedera.node.app.service.token.impl.validators.TokenAirdropValidator;
import com.hedera.node.app.service.token.records.CryptoTransferStreamBuilder;
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 edu.umd.cs.findbugs.annotations.NonNull;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * This class contains all workflow-related functionality regarding {@link
 * HederaFunctionality#TOKEN_CLAIM_AIRDROP}.
 */
@Singleton
public class TokenClaimAirdropHandler extends TransferExecutor implements TransactionHandler {
    private final TokenAirdropValidator validator;
    private final PendingAirdropUpdater pendingAirdropUpdater;

    @Inject
    public TokenClaimAirdropHandler(
            @NonNull final TokenAirdropValidator validator,
            @NonNull final CryptoTransferValidator cryptoTransferValidator,
            @NonNull final PendingAirdropUpdater pendingAirdropUpdater) {
        super(cryptoTransferValidator);
        this.validator = validator;
        this.pendingAirdropUpdater = pendingAirdropUpdater;
    }

    @Override
    public void preHandle(@NonNull PreHandleContext context) throws PreCheckException {
        requireNonNull(context);
        final var op = requireNonNull(context.body().tokenClaimAirdrop());
        final var pendingAirdrops = op.pendingAirdrops();

        for (final var pendingAirdrop : pendingAirdrops) {
            final var receiverId = pendingAirdrop.receiverIdOrThrow();
            context.requireAliasedKeyOrThrow(receiverId, INVALID_ACCOUNT_ID);
        }
    }

    @Override
    public void pureChecks(@NonNull TransactionBody txn) throws PreCheckException {
        requireNonNull(txn);

        final var op = txn.tokenClaimAirdrop();
        requireNonNull(op);

        final var pendingAirdrops = op.pendingAirdrops();
        validateTruePreCheck(!pendingAirdrops.isEmpty(), EMPTY_PENDING_AIRDROP_ID_LIST);

        final var uniqueAirdrops = Set.copyOf(pendingAirdrops);
        validateTruePreCheck(pendingAirdrops.size() == uniqueAirdrops.size(), PENDING_AIRDROP_ID_REPEATED);
    }

    @Override
    public void handle(@NonNull HandleContext context) throws HandleException {
        final var op = context.body().tokenClaimAirdropOrThrow();

        final var pendingAirdropStore = context.storeFactory().writableStore(WritableAirdropStore.class);
        final var accountStore = context.storeFactory().writableStore(WritableAccountStore.class);
        final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class);
        final var tokenRelStore = context.storeFactory().writableStore(WritableTokenRelationStore.class);
        final var recordBuilder = context.savepointStack().getBaseBuilder(CryptoTransferStreamBuilder.class);

        final var validatedAirdropIds = validateSemantics(context, op, accountStore);

        final Map transfers = new HashMap<>();
        final var tokensToAssociate = new LinkedHashMap>();

        // 1. validate pending airdrops and create transfer lists
        for (var airdropId : validatedAirdropIds) {
            final var tokenId = airdropId.hasFungibleTokenType()
                    ? airdropId.fungibleTokenTypeOrThrow()
                    : airdropId.nonFungibleTokenOrThrow().tokenIdOrThrow();
            final var senderId = airdropId.senderIdOrThrow();
            final var receiverId = airdropId.receiverIdOrThrow();

            // Merge this transfer by token id into the transfers map
            createOrUpdateTransfers(airdropId, pendingAirdropStore, tokenId, senderId, receiverId, transfers);

            // check if we need new association
            if (tokenRelStore.get(receiverId, tokenId) == null) {
                tokensToAssociate
                        .computeIfAbsent(receiverId, k -> new ArrayList<>())
                        .add(getIfUsable(tokenId, tokenStore));
            }
        }
        for (var entry : tokensToAssociate.entrySet()) {
            associateForFree(entry.getValue(), entry.getKey(), accountStore, tokenRelStore);
        }
        // do the crypto transfer
        transferForFree(new ArrayList<>(transfers.values()), context, recordBuilder);
        pendingAirdropUpdater.removePendingAirdrops(validatedAirdropIds, pendingAirdropStore, accountStore);
    }

    /**
     * Validates the semantics of the token claim airdrop transaction.
     *
     * @param context the handle context
     * @param op the token claim airdrop transaction body
     * @param accountStore the account store
     * @return a list of validated pending airdrop ids using the {@code 0.0.X} reference for both sender and receiver
     * @throws HandleException if the transaction is invalid
     */
    private List validateSemantics(
            @NonNull HandleContext context,
            @NonNull TokenClaimAirdropTransactionBody op,
            @NonNull final ReadableAccountStore accountStore)
            throws HandleException {
        final var tokensConfig = context.configuration().getConfigData(TokensConfig.class);
        validateTrue(
                op.pendingAirdrops().size() <= tokensConfig.maxAllowedPendingAirdropsToClaim(),
                PENDING_AIRDROP_ID_LIST_TOO_LONG);

        final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class);
        final var pendingAirdropStore = context.storeFactory().readableStore(ReadableAirdropStore.class);
        final var standardAirdropIds = AirdropHandlerHelper.standardizeAirdropIds(
                accountStore,
                pendingAirdropStore,
                op.pendingAirdrops(),
                EnumSet.of(AirdropHandlerHelper.IdType.RECEIVER));
        for (final var airdrop : standardAirdropIds) {
            final var tokenId = airdrop.hasFungibleTokenType()
                    ? airdrop.fungibleTokenTypeOrThrow()
                    : airdrop.nonFungibleTokenOrThrow().tokenIdOrThrow();
            getIfUsable(tokenId, tokenStore);
            validateTrue(
                    validator.tokenHasNoRoyaltyWithFallbackFee(tokenId, tokenStore),
                    TOKEN_AIRDROP_WITH_FALLBACK_ROYALTY);
        }
        return standardAirdropIds;
    }

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

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

    private void createOrUpdateTransfers(
            @NonNull final PendingAirdropId airdrop,
            @NonNull final WritableAirdropStore airdropStore,
            @NonNull final TokenID tokenId,
            @NonNull final AccountID senderId,
            @NonNull final AccountID receiverId,
            @NonNull final Map transfers) {
        final var accountPendingAirdrop = requireNonNull(airdropStore.get(airdrop));
        final var soFar = transfers.computeIfAbsent(
                tokenId, k -> TokenTransferList.newBuilder().token(tokenId).build());
        if (airdrop.hasFungibleTokenType()) {
            // process fungible tokens
            final var senderAccountAmount = asAccountAmount(
                    senderId,
                    -accountPendingAirdrop.pendingAirdropValueOrThrow().amount());
            final var receiverAccountAmount = asAccountAmount(
                    receiverId,
                    accountPendingAirdrop.pendingAirdropValueOrThrow().amount());
            final List newTransfers = new ArrayList<>(soFar.transfers());
            mergeTransfer(newTransfers, senderAccountAmount);
            mergeTransfer(newTransfers, receiverAccountAmount);
            transfers.put(tokenId, soFar.copyBuilder().transfers(newTransfers).build());
        } else {
            // process non-fungible tokens
            final var nftTransfer = NftTransfer.newBuilder()
                    .senderAccountID(senderId)
                    .receiverAccountID(receiverId)
                    .serialNumber(airdrop.nonFungibleTokenOrThrow().serialNumber())
                    .build();
            final List newTransfers = new ArrayList<>(soFar.nftTransfers());
            newTransfers.add(nftTransfer);
            transfers.put(
                    tokenId, soFar.copyBuilder().nftTransfers(newTransfers).build());
        }
    }

    private void mergeTransfer(@NonNull final List transfers, @NonNull final AccountAmount newTransfer) {
        final var accountId = newTransfer.accountIDOrThrow();
        for (int i = 0, n = transfers.size(); i < n; i++) {
            if (transfers.get(i).accountIDOrThrow().equals(accountId)) {
                final var updatedTransfer = transfers
                        .get(i)
                        .copyBuilder()
                        .amount(transfers.get(i).amount() + newTransfer.amount())
                        .build();
                transfers.set(i, updatedTransfer);
                return;
            }
        }
        transfers.add(newTransfer);
    }

    private void associateForFree(
            @NonNull final List tokensToAssociate,
            @NonNull final AccountID receiverId,
            @NonNull final WritableAccountStore accountStore,
            @NonNull final WritableTokenRelationStore tokenRelStore) {
        createAndLinkTokenRels(
                requireNonNull(accountStore.getAccountById(receiverId)),
                tokensToAssociate,
                accountStore,
                tokenRelStore);
    }

    private void transferForFree(
            @NonNull final List transfers,
            @NonNull final HandleContext context,
            @NonNull final CryptoTransferStreamBuilder recordBuilder) {
        final var cryptoTransferBody = CryptoTransferTransactionBody.newBuilder()
                .tokenTransfers(transfers)
                .build();
        final var syntheticCryptoTransferTxn =
                TransactionBody.newBuilder().cryptoTransfer(cryptoTransferBody).build();
        final var transferContext = new TransferContextImpl(context, cryptoTransferBody, true);
        // We should skip custom fee steps here, because they must be already prepaid
        executeCryptoTransferWithoutCustomFee(syntheticCryptoTransferTxn, transferContext, context, recordBuilder);
    }

    public static AccountAmount asAccountAmount(final AccountID account, final long amount) {
        return AccountAmount.newBuilder().accountID(account).amount(amount).build();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy