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

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

There is a newer version: 0.56.6
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.EMPTY_ALLOWANCES;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ALLOWANCE_OWNER_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_NFT_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_PAYER_ACCOUNT_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.SENDER_DOES_NOT_OWN_NFT_SERIAL_NO;
import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.LONG_SIZE;
import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.NFT_DELETE_ALLOWANCE_SIZE;
import static com.hedera.node.app.service.token.impl.validators.AllowanceValidator.isValidOwner;
import static com.hedera.node.app.service.token.impl.validators.ApproveAllowanceValidator.getEffectiveOwner;
import static com.hedera.node.app.spi.validation.Validations.mustExist;
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.AccountID;
import com.hedera.hapi.node.base.HederaFunctionality;
import com.hedera.hapi.node.base.SubType;
import com.hedera.hapi.node.base.TokenID;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.hapi.node.token.NftRemoveAllowance;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.service.token.ReadableAccountStore;
import com.hedera.node.app.service.token.impl.WritableAccountStore;
import com.hedera.node.app.service.token.impl.WritableNftStore;
import com.hedera.node.app.service.token.impl.WritableTokenStore;
import com.hedera.node.app.service.token.impl.validators.DeleteAllowanceValidator;
import com.hedera.node.app.spi.fees.FeeContext;
import com.hedera.node.app.spi.fees.Fees;
import com.hedera.node.app.spi.validation.ExpiryValidator;
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 edu.umd.cs.findbugs.annotations.NonNull;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * This class contains all workflow-related functionality regarding {@link
 * HederaFunctionality#CRYPTO_DELETE_ALLOWANCE}. Crypto delete allowance is used to
 * delete an existing allowance for a given NFT serial number.
 */
@Singleton
public class CryptoDeleteAllowanceHandler implements TransactionHandler {
    private final DeleteAllowanceValidator deleteAllowanceValidator;

    /**
     * Default constructor for injection.
     * @param validator the validator for validating a delete allowance transaction
     */
    @Inject
    public CryptoDeleteAllowanceHandler(@NonNull final DeleteAllowanceValidator validator) {
        this.deleteAllowanceValidator = validator;
    }

    @Override
    public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException {
        requireNonNull(txn);
        final var op = txn.cryptoDeleteAllowanceOrThrow();
        final var allowances = op.nftAllowances();
        validateTruePreCheck(!allowances.isEmpty(), EMPTY_ALLOWANCES);
        for (final var allowance : allowances) {
            mustExist(allowance.tokenId(), INVALID_TOKEN_ID);
            validateTruePreCheck(!allowance.serialNumbers().isEmpty(), EMPTY_ALLOWANCES);
        }
    }

    @Override
    public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException {
        requireNonNull(context);
        final var txn = context.body();
        final var op = txn.cryptoDeleteAllowanceOrThrow();
        // Every owner whose allowances are being removed should sign (or the payer, if there is no owner)
        for (final var allowance : op.nftAllowances()) {
            if (allowance.hasOwner()) {
                final var store = context.createStore(ReadableAccountStore.class);
                final var ownerId = allowance.ownerOrThrow();
                final var owner = store.getAccountById(ownerId);
                final var approvedForAll = owner.approveForAllNftAllowances().stream()
                        .anyMatch(approveForAll -> approveForAll.tokenId().equals(allowance.tokenId())
                                && approveForAll.spenderId().equals(context.payer()));
                if (!context.payer().equals(ownerId) && !approvedForAll) {
                    context.requireKeyOrThrow(ownerId, INVALID_ALLOWANCE_OWNER_ID);
                }
            }
        }
    }

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

        final var accountStore = context.writableStore(WritableAccountStore.class);
        // validate payer account exists
        final var payerAccount = accountStore.getAccountById(payer);
        validateTrue(payerAccount != null, INVALID_PAYER_ACCOUNT_ID);

        // validate the transaction body fields that include state or configuration
        // We can use payerAccount for validations since it's not mutated in validateSemantics
        validateSemantics(context, payerAccount, accountStore);

        // Apply all changes to the state modifications
        deleteAllowance(context, payerAccount, accountStore);
    }

    /**
     * Deletes allowance for the given nft serials.
     * Clears spender on the provided nft serials if the owner owns the serials.
     * If owner is not specified payer is considered as the owner.
     * If the owner doesn't own these serials throws an exception.
     * @param context handle context
     * @param payer payer for the transaction
     * @param accountStore account store
     * @throws HandleException if any of the nft serials are not owned by owner
     */
    private void deleteAllowance(
            @NonNull final HandleContext context,
            @NonNull final Account payer,
            @NonNull final WritableAccountStore accountStore)
            throws HandleException {
        requireNonNull(context);
        requireNonNull(payer);
        requireNonNull(accountStore);

        final var op = context.body().cryptoDeleteAllowanceOrThrow();
        final var nftAllowances = op.nftAllowances();

        final var nftStore = context.writableStore(WritableNftStore.class);
        final var tokenStore = context.writableStore(WritableTokenStore.class);

        deleteNftSerials(nftAllowances, payer, accountStore, tokenStore, nftStore, context.expiryValidator());
    }

    /**
     * Clear spender on the provided nft serials. If the owner is not provided in any allowance,
     * considers payer of the transaction as owner while checking if nft is owned by owner.
     * @param nftAllowances given nftAllowances
     * @param payerAccount payer for the transaction
     * @param accountStore account Store
     * @param tokenStore token Store
     * @param nftStore nft Store
     */
    private void deleteNftSerials(
            final List nftAllowances,
            final Account payerAccount,
            final WritableAccountStore accountStore,
            final WritableTokenStore tokenStore,
            final WritableNftStore nftStore,
            @NonNull final ExpiryValidator expiryValidator)
            throws HandleException {
        if (nftAllowances.isEmpty()) {
            return;
        }
        for (final var allowance : nftAllowances) {
            final var serialNums = allowance.serialNumbers();
            final var tokenId = allowance.tokenIdOrElse(TokenID.DEFAULT);
            // If owner is not provided in allowance, consider payer as owner
            final var owner = getEffectiveOwner(allowance.owner(), payerAccount, accountStore, expiryValidator);
            final var token = tokenStore.get(tokenId);
            for (final var serial : serialNums) {
                final var nft = nftStore.get(tokenId, serial);

                validateTrue(nft != null, INVALID_NFT_ID);
                final AccountID accountOwner = owner.accountId();
                validateTrue(isValidOwner(nft, accountOwner, token), SENDER_DOES_NOT_OWN_NFT_SERIAL_NO);

                // Null out spender on the nft
                nftStore.put(nft.copyBuilder().spenderId((AccountID) null).build());
            }
        }
    }

    /**
     * Validate the transaction body fields that include state or configuration.
     * We can use payerAccount for validations since it's not mutated in validateSemantics.
     * @param context the context of the transaction
     * @param payerAccount the account of the payer
     * @param accountStore the account store
     */
    private void validateSemantics(
            @NonNull final HandleContext context, final Account payerAccount, final ReadableAccountStore accountStore) {
        requireNonNull(context);
        requireNonNull(payerAccount);
        requireNonNull(accountStore);

        final var op = context.body().cryptoDeleteAllowanceOrThrow();
        final var nftAllowances = op.nftAllowances();

        deleteAllowanceValidator.validate(context, nftAllowances, payerAccount, accountStore);
    }

    @NonNull
    @Override
    public Fees calculateFees(@NonNull final FeeContext feeContext) {
        final var body = feeContext.body();
        final var op = body.cryptoDeleteAllowanceOrThrow();
        return feeContext
                .feeCalculator(SubType.DEFAULT)
                .addBytesPerTransaction(op.nftAllowances().size() * NFT_DELETE_ALLOWANCE_SIZE
                        + countNftDeleteSerials(op.nftAllowances()) * LONG_SIZE)
                .calculate();
    }

    private int countNftDeleteSerials(final List nftAllowancesList) {
        int totalSerials = 0;
        for (var allowance : nftAllowancesList) {
            totalSerials += allowance.serialNumbers().size();
        }
        return totalSerials;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy