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

com.hedera.node.app.service.token.impl.handlers.CryptoApproveAllowanceHandler 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_ALLOWANCE_SPENDER_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_DELEGATING_SPENDER;
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.NEGATIVE_ALLOWANCE_AMOUNT;
import static com.hedera.hapi.node.base.ResponseCodeEnum.SENDER_DOES_NOT_OWN_NFT_SERIAL_NO;
import static com.hedera.node.app.hapi.fees.usage.SingletonEstimatorUtils.ESTIMATOR_UTILS;
import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.CRYPTO_ALLOWANCE_SIZE;
import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.LONG_SIZE;
import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.NFT_ALLOWANCE_SIZE;
import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.TOKEN_ALLOWANCE_SIZE;
import static com.hedera.node.app.service.token.impl.validators.AllowanceValidator.isValidOwner;
import static com.hedera.node.app.service.token.impl.validators.AllowanceValidator.validateAllowanceLimit;
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.validation.Validations.validateNullableAccountID;
import static com.hedera.node.app.spi.workflows.HandleException.validateTrue;
import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck;
import static java.util.Collections.emptyList;
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.state.token.AccountApprovalForAllAllowance;
import com.hedera.hapi.node.state.token.AccountCryptoAllowance;
import com.hedera.hapi.node.state.token.AccountFungibleTokenAllowance;
import com.hedera.hapi.node.token.CryptoAllowance;
import com.hedera.hapi.node.token.CryptoApproveAllowanceTransactionBody;
import com.hedera.hapi.node.token.NftAllowance;
import com.hedera.hapi.node.token.TokenAllowance;
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.ApproveAllowanceValidator;
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.HederaConfig;
import com.swirlds.base.utility.Pair;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * This class contains all workflow-related functionality regarding
 * {@link HederaFunctionality#CRYPTO_APPROVE_ALLOWANCE}.
 */
@Singleton
public class CryptoApproveAllowanceHandler implements TransactionHandler {
    private final ApproveAllowanceValidator allowanceValidator;

    /**
     * Constructs a {@link CryptoApproveAllowanceHandler} with the given {@link ApproveAllowanceValidator}.
     * @param allowanceValidator the validator to use for validating the transaction
     */
    @Inject
    public CryptoApproveAllowanceHandler(@NonNull final ApproveAllowanceValidator allowanceValidator) {
        this.allowanceValidator = allowanceValidator;
    }

    /**
     * @param txn the transaction body
     * @throws PreCheckException if the transaction is invalid for any reason
     */
    @Override
    public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException {
        requireNonNull(txn);
        final var op = txn.cryptoApproveAllowanceOrThrow();

        // The transaction must have at least one type of allowance. There is also an upper limit to the allowed number
        // of allowances in a single transaction, but that check requires the config, and is thus not a pure check.
        // So we will check that later in handle.
        final var cryptoAllowances = op.cryptoAllowances();
        final var tokenAllowances = op.tokenAllowances();
        final var nftAllowances = op.nftAllowances();
        final var totalAllowancesSize = cryptoAllowances.size() + tokenAllowances.size() + nftAllowances.size();
        validateTruePreCheck(totalAllowancesSize != 0, EMPTY_ALLOWANCES);

        // It is OK for the owner to be null, because that just means that we should use the payer as the owner.
        // But the spender always needs to be specified.
        for (final var allowance : cryptoAllowances) {
            validateNullableAccountID(allowance.owner());
            validateTruePreCheck(allowance.amount() >= 0, NEGATIVE_ALLOWANCE_AMOUNT);
            validateAccountID(allowance.spender(), INVALID_ALLOWANCE_SPENDER_ID);
        }

        for (final var allowance : tokenAllowances) {
            validateNullableAccountID(allowance.owner());
            validateTruePreCheck(allowance.amount() >= 0, NEGATIVE_ALLOWANCE_AMOUNT);
            validateAccountID(allowance.spender(), INVALID_ALLOWANCE_SPENDER_ID);
            mustExist(allowance.tokenId(), INVALID_TOKEN_ID);
        }

        for (final var allowance : nftAllowances) {
            validateNullableAccountID(allowance.owner());
            validateAccountID(allowance.spender(), INVALID_ALLOWANCE_SPENDER_ID);
            mustExist(allowance.tokenId(), INVALID_TOKEN_ID);
        }
    }

    @Override
    public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException {
        requireNonNull(context);
        final var txn = context.body();
        final var payerId = context.payer();
        final var op = txn.cryptoApproveAllowanceOrThrow();

        // If the allowance owner is not the payer, then the transaction must have been signed by the owner. Note, if
        // the owner is missing, then the owner is assumed to be the payer. This is true for crypto allowances and
        // for token allowances.
        for (final var allowance : op.cryptoAllowances()) {
            final var owner = allowance.owner();
            if (owner != null && !owner.equals(payerId)) {
                context.requireKeyOrThrow(owner, INVALID_ALLOWANCE_OWNER_ID);
            }
        }

        // Fungible token allowances are the same as basic crypto approvals and allowances
        for (final var allowance : op.tokenAllowances()) {
            final var owner = allowance.owner();
            // (TEMPORARY) Remove after diff testing is complete
            if (owner != null && owner.hasAlias()) {
                throw new PreCheckException(INVALID_ALLOWANCE_OWNER_ID);
            }
            if (owner != null && !owner.equals(payerId)) {
                context.requireKeyOrThrow(owner, INVALID_ALLOWANCE_OWNER_ID);
            }
        }

        // NFT allowances are a little more complicated because they have delegating spenders and approvedForAll.
        for (final var allowance : op.nftAllowances()) {
            // Only the owner can grant approvedForAll, so if approvedForAll is true, then the owner must sign.
            // If approvedForAll is false, and if there is a delegating spender, then they must sign. Otherwise,
            // the owner must sign.
            final var ownerId = allowance.owner();
            final boolean approvedForAll = allowance.approvedForAllOrElse(false);
            final var operatorId = approvedForAll ? ownerId : allowance.delegatingSpenderOrElse(ownerId);
            // Now that we know who should sign, if that account is not the payer, then we need to require that
            // key. If there is an error, then we need to use the appropriate error code depending on whether
            // the operator is the owner or the delegating spender.
            if (operatorId != null && !operatorId.equals(payerId)) {
                final var error = ownerId == operatorId ? INVALID_ALLOWANCE_OWNER_ID : INVALID_DELEGATING_SPENDER;
                context.requireKeyOrThrow(operatorId, error);
            }
        }
    }

    @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.
        validateSemantics(context, payerAccount, accountStore);

        // Apply all changes to the state modifications. We need to look up payer for each modification, since payer
        // would have been modified by a previous allowance change
        approveAllowance(context, payer, accountStore);
    }
    /**
     * Validate the transaction body fields that include state or configuration
     * @param context the handle context
     * @param payerAccount the payer account
     * @param accountStore the account store
     */
    private void validateSemantics(
            @NonNull final HandleContext context,
            @NonNull final Account payerAccount,
            @NonNull final ReadableAccountStore accountStore) {
        requireNonNull(context);
        requireNonNull(payerAccount);
        requireNonNull(accountStore);

        allowanceValidator.validate(context, payerAccount, accountStore);
    }

    /**
     * Apply all changes to the state modifications for crypto, token, and nft allowances
     * @param context the handle context
     * @param payerId the payer account id
     * @param accountStore the account store
     * @throws HandleException if there is an error applying the changes
     */
    private void approveAllowance(
            @NonNull final HandleContext context,
            @NonNull final AccountID payerId,
            @NonNull final WritableAccountStore accountStore)
            throws HandleException {
        requireNonNull(context);
        requireNonNull(payerId);
        requireNonNull(accountStore);

        final var op = context.body().cryptoApproveAllowanceOrThrow();
        final var cryptoAllowances = op.cryptoAllowances();
        final var tokenAllowances = op.tokenAllowances();
        final var nftAllowances = op.nftAllowances();

        final var hederaConfig = context.configuration().getConfigData(HederaConfig.class);
        final var tokenStore = context.writableStore(WritableTokenStore.class);
        final var uniqueTokenStore = context.writableStore(WritableNftStore.class);

        /* --- Apply changes to state --- */
        final var allowanceMaxAccountLimit = hederaConfig.allowancesMaxAccountLimit();
        applyCryptoAllowances(cryptoAllowances, payerId, accountStore, allowanceMaxAccountLimit);
        applyFungibleTokenAllowances(tokenAllowances, payerId, accountStore, allowanceMaxAccountLimit);
        applyNftAllowances(
                nftAllowances, payerId, accountStore, tokenStore, uniqueTokenStore, allowanceMaxAccountLimit);
    }

    /**
     * Applies all changes needed for Crypto allowances from the transaction.
     * If the spender already has an allowance, the allowance value will be replaced with values
     * from transaction. If the amount specified is 0, the allowance will be removed.
     * @param cryptoAllowances the list of crypto allowances
     * @param payerId the payer account id
     * @param accountStore the account store
     * @param allowanceMaxAccountLimit the {@link HederaConfig}
     */
    private void applyCryptoAllowances(
            @NonNull final List cryptoAllowances,
            @NonNull final AccountID payerId,
            @NonNull final WritableAccountStore accountStore,
            final int allowanceMaxAccountLimit) {
        requireNonNull(cryptoAllowances);
        requireNonNull(payerId);
        requireNonNull(accountStore);

        for (final var allowance : cryptoAllowances) {
            final var owner = allowance.owner();
            final var spender = allowance.spenderOrThrow();
            final var effectiveOwner = getEffectiveOwnerAccount(owner, payerId, accountStore);
            final var mutableAllowances = new ArrayList<>(effectiveOwner.cryptoAllowances());

            final var amount = allowance.amount();

            updateCryptoAllowance(mutableAllowances, amount, spender);
            final var copy = effectiveOwner
                    .copyBuilder()
                    .cryptoAllowances(mutableAllowances)
                    .build();
            // Only when amount is greater than 0 we add or modify existing allowances.
            // When removing existing allowances no need to check this.
            if (amount > 0) {
                validateAllowanceLimit(copy, allowanceMaxAccountLimit);
            }
            accountStore.put(copy);
        }
    }

    /**
     * Updates the crypto allowance amount if the allowance exists, otherwise adds a new allowance
     * If the amount is zero removes the allowance if it exists in the list
     * @param mutableAllowances the list of mutable allowances of owner
     * @param amount the amount
     * @param spenderId the spender id
     */
    private void updateCryptoAllowance(
            final ArrayList mutableAllowances, final long amount, final AccountID spenderId) {
        final var newAllowanceBuilder = AccountCryptoAllowance.newBuilder().spenderId(spenderId);
        // get the index of the allowance with same spender in existing list
        final var index = lookupSpender(mutableAllowances, spenderId);
        // If given amount is zero, if the element exists remove it, otherwise do nothing
        if (amount == 0) {
            if (index != -1) {
                // If amount is 0, remove the allowance
                mutableAllowances.remove(index);
            }
            return;
        }
        if (index != -1) {
            mutableAllowances.set(index, newAllowanceBuilder.amount(amount).build());
        } else {
            mutableAllowances.add(newAllowanceBuilder.amount(amount).build());
        }
    }

    /**
     * Applies all changes needed for fungible token allowances from the transaction.If the key
     * {token, spender} already has an allowance, the allowance value will be replaced with values
     * from transaction
     * @param tokenAllowances the list of token allowances
     * @param payerId the payer account id
     * @param accountStore the account store
     * @param allowanceMaxAccountLimit the {@link HederaConfig} allowance max account limit
     */
    private void applyFungibleTokenAllowances(
            @NonNull final List tokenAllowances,
            @NonNull final AccountID payerId,
            @NonNull final WritableAccountStore accountStore,
            final int allowanceMaxAccountLimit) {
        for (final var allowance : tokenAllowances) {
            final var owner = allowance.owner();
            final var amount = allowance.amount();
            final var tokenId = allowance.tokenIdOrThrow();
            final var spender = allowance.spenderOrThrow();

            final var effectiveOwner = getEffectiveOwnerAccount(owner, payerId, accountStore);
            final var mutableTokenAllowances = new ArrayList<>(effectiveOwner.tokenAllowances());

            updateTokenAllowance(mutableTokenAllowances, amount, spender, tokenId);
            final var copy = effectiveOwner
                    .copyBuilder()
                    .tokenAllowances(mutableTokenAllowances)
                    .build();
            // Only when amount is greater than 0 we add or modify existing allowances.
            // When removing existing allowances no need to check this.
            if (amount > 0) {
                validateAllowanceLimit(copy, allowanceMaxAccountLimit);
            }
            accountStore.put(copy);
        }
    }

    /**
     * Updates the token allowance amount if the allowance for given tokenNuma dn spenderNum exists,
     * otherwise adds a new allowance.
     * If the amount is zero removes the allowance if it exists in the list
     * @param mutableAllowances the list of mutable allowances of owner
     * @param amount the amount
     * @param spenderId the spender number
     * @param tokenId the token number
     */
    private void updateTokenAllowance(
            final ArrayList mutableAllowances,
            final long amount,
            final AccountID spenderId,
            final TokenID tokenId) {
        final var newAllowanceBuilder =
                AccountFungibleTokenAllowance.newBuilder().spenderId(spenderId).tokenId(tokenId);
        final var index = lookupSpenderAndToken(mutableAllowances, spenderId, tokenId);
        // If given amount is zero, if the element exists remove it
        if (amount == 0) {
            if (index != -1) {
                mutableAllowances.remove(index);
            }
            return;
        }
        if (index != -1) {
            mutableAllowances.set(index, newAllowanceBuilder.amount(amount).build());
        } else {
            mutableAllowances.add(newAllowanceBuilder.amount(amount).build());
        }
    }

    /**
     * Applies all changes needed for NFT allowances from the transaction. If the key{tokenNum,
     * spenderNum} doesn't exist in the map the allowance will be inserted. If the key exists,
     * existing allowance values will be replaced with new allowances given in operation
     *
     * @param nftAllowances the list of nft allowances
     * @param payerId the payer account id
     * @param accountStore the account store
     * @param tokenStore the token store
     * @param uniqueTokenStore the unique token store
     * @param allowanceMaxAccountLimit the {@link HederaConfig} config allowance max account limit
     */
    protected void applyNftAllowances(
            final List nftAllowances,
            @NonNull final AccountID payerId,
            @NonNull final WritableAccountStore accountStore,
            @NonNull final WritableTokenStore tokenStore,
            @NonNull final WritableNftStore uniqueTokenStore,
            final int allowanceMaxAccountLimit) {
        for (final var allowance : nftAllowances) {
            final var owner = allowance.owner();
            final var tokenId = allowance.tokenIdOrThrow();
            final var spender = allowance.spenderOrThrow();

            final var effectiveOwner = getEffectiveOwnerAccount(owner, payerId, accountStore);
            final var mutableNftAllowances = new ArrayList<>(effectiveOwner.approveForAllNftAllowances());

            if (allowance.hasApprovedForAll()) {
                final var approveForAllAllowance = AccountApprovalForAllAllowance.newBuilder()
                        .tokenId(tokenId)
                        .spenderId(spender)
                        .build();

                if (Boolean.TRUE.equals(allowance.approvedForAll())) {
                    if (!mutableNftAllowances.contains(approveForAllAllowance)) {
                        mutableNftAllowances.add(approveForAllAllowance);
                    }
                } else {
                    mutableNftAllowances.remove(approveForAllAllowance);
                }
                final var copy = effectiveOwner
                        .copyBuilder()
                        .approveForAllNftAllowances(mutableNftAllowances)
                        .build();
                validateAllowanceLimit(copy, allowanceMaxAccountLimit);
                accountStore.put(copy);
            }
            // Update spender on all these Nfts to the new spender
            updateSpender(tokenStore, uniqueTokenStore, effectiveOwner, spender, tokenId, allowance.serialNumbers());
        }
    }

    /**
     * Updates the spender of the given NFTs to the new spender
     * @param tokenStore the token store
     * @param uniqueTokenStore the unique token store
     * @param owner the owner account
     * @param spenderId the spender account id
     * @param tokenId the token id
     * @param serialNums the serial numbers
     */
    public void updateSpender(
            @NonNull final WritableTokenStore tokenStore,
            @NonNull final WritableNftStore uniqueTokenStore,
            @NonNull final Account owner,
            @NonNull final AccountID spenderId,
            @NonNull final TokenID tokenId,
            @NonNull final List serialNums) {
        if (serialNums.isEmpty()) {
            return;
        }

        final var serialsSet = new HashSet<>(serialNums);
        for (final var serialNum : serialsSet) {
            final var nft = uniqueTokenStore.get(tokenId, serialNum);
            final var token = tokenStore.get(tokenId);

            final AccountID accountOwner = owner.accountId();
            validateTrue(isValidOwner(nft, accountOwner, token), SENDER_DOES_NOT_OWN_NFT_SERIAL_NO);
            final var copy = nft.copyBuilder().spenderId(spenderId).build();
            uniqueTokenStore.put(copy);
        }
    }

    /**
     * Returns the index of the allowance with the given spender in the list if it exists,
     * otherwise returns -1
     * @param ownerAllowances list of allowances
     * @param spenderNum spender account number
     * @return index of the allowance if it exists, otherwise -1
     */
    private int lookupSpender(final List ownerAllowances, final AccountID spenderNum) {
        for (int i = 0; i < ownerAllowances.size(); i++) {
            final var allowance = ownerAllowances.get(i);
            if (allowance.spenderId().equals(spenderNum)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Returns the index of the allowance  with the given spender and token in the list if it exists,
     * otherwise returns -1
     * @param ownerAllowances list of allowances
     * @param spenderId spender account number
     * @param tokenId token number
     * @return index of the allowance if it exists, otherwise -1
     */
    private int lookupSpenderAndToken(
            final List ownerAllowances,
            final AccountID spenderId,
            final TokenID tokenId) {
        for (int i = 0; i < ownerAllowances.size(); i++) {
            final var allowance = ownerAllowances.get(i);
            if (allowance.spenderId().equals(spenderId) && allowance.tokenId().equals(tokenId)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Returns the effective owner account. If the owner is not present or owner is same as payer.
     * Since we are modifying the payer account in the same transaction for each allowance if owner is not specified,
     * we need to get the payer account each time from the modifications map.
     * @param owner owner of the allowance
     * @param payerId payer of the transaction
     * @param accountStore account store
     * @return effective owner account
     */
    private static Account getEffectiveOwnerAccount(
            @Nullable final AccountID owner,
            @NonNull final AccountID payerId,
            @NonNull final ReadableAccountStore accountStore) {
        final var ownerNum = owner != null ? owner.accountNumOrElse(0L) : 0L;
        if (ownerNum == 0 || ownerNum == payerId.accountNumOrThrow()) {
            // The payer would have been modified in the same transaction for previous allowances
            // So, get it from modifications map.
            return accountStore.getAccountById(payerId);
        } else {
            // If owner is in modifications get the modified account from state
            final var ownerAccount = accountStore.getAccountById(owner);
            validateTrue(ownerAccount != null, INVALID_ALLOWANCE_OWNER_ID);
            return ownerAccount;
        }
    }

    @NonNull
    @Override
    public Fees calculateFees(@NonNull final FeeContext feeContext) {
        final var body = feeContext.body();
        final var op = body.cryptoApproveAllowanceOrThrow();
        final var accountStore = feeContext.readableStore(ReadableAccountStore.class);

        final var currentSecond =
                body.transactionIDOrThrow().transactionValidStartOrThrow().seconds();
        final var account = accountStore.getAccountById(feeContext.payer());

        final var currentExpiry = account == null ? currentSecond : account.expirationSecond();
        final long lifeTime = ESTIMATOR_UTILS.relativeLifetime(currentSecond, currentExpiry);
        // If the value is being adjusted instead of inserting a new entry , the fee charged will be
        // slightly less than the base price
        final var adjustedBytes = getNewBytes(body.cryptoApproveAllowanceOrThrow(), account);
        return feeContext
                .feeCalculator(SubType.DEFAULT)
                .addBytesPerTransaction(bytesUsedInTxn(op))
                .addRamByteSeconds(adjustedBytes > 0 ? (adjustedBytes * lifeTime) : 0)
                .calculate();
    }

    /**
     * Gets total bytes used in transaction
     * @param op the crypto approve allowance transaction body
     * @return the total bytes used in transaction
     */
    private int bytesUsedInTxn(final CryptoApproveAllowanceTransactionBody op) {
        return op.cryptoAllowances().size() * CRYPTO_ALLOWANCE_SIZE
                + op.tokenAllowances().size() * TOKEN_ALLOWANCE_SIZE
                + op.nftAllowances().size() * NFT_ALLOWANCE_SIZE
                + countSerials(op.nftAllowances()) * LONG_SIZE;
    }

    /**
     * Gets the new bytes that will be added to state from the transaction, if it is successful compared to
     * what is already present in state
     * @param op the crypto approve allowance transaction body
     * @param account the account existing in state
     * @return the new bytes that will be added to state
     */
    private long getNewBytes(final CryptoApproveAllowanceTransactionBody op, final Account account) {
        final long newCryptoKeys =
                getChangedCryptoKeys(op.cryptoAllowances(), account == null ? emptyList() : account.cryptoAllowances());
        final long newTokenKeys =
                getChangedTokenKeys(op.tokenAllowances(), account == null ? emptyList() : account.tokenAllowances());
        final long newApproveForAllNfts = getChangedNftKeys(
                op.nftAllowances(), account == null ? emptyList() : account.approveForAllNftAllowances());

        return newCryptoKeys * CRYPTO_ALLOWANCE_SIZE
                + newTokenKeys * TOKEN_ALLOWANCE_SIZE
                + newApproveForAllNfts * NFT_ALLOWANCE_SIZE;
    }

    /**
     * Gets the number of new crypto allowances that will be added to state from the transaction.
     * @param newAllowances the list of new crypto allowances
     * @param existingAllowances the list of existing crypto allowances
     * @return the number of new crypto allowances that will be added to state from the transaction
     */
    private int getChangedCryptoKeys(
            final List newAllowances, final List existingAllowances) {
        int counter = 0;
        final var existingSpenders = existingAllowances.stream()
                .map(AccountCryptoAllowance::spenderId)
                .collect(Collectors.toSet());
        final var newSpenders = new HashSet();
        for (var key : newAllowances) {
            if (!existingSpenders.contains(key.spender()) && !newSpenders.contains(key.spender())) {
                newSpenders.add(key.spender());
                counter++;
            }
        }
        return counter;
    }

    /**
     * Gets the number of new token allowances that will be added to state from the transaction.
     * @param newAllowances the list of new token allowances
     * @param existingAllowances the list of existing token allowances
     * @return the number of new token allowances that will be added to state from the transaction
     */
    private int getChangedTokenKeys(
            final List newAllowances, final List existingAllowances) {
        int counter = 0;
        final var existingKeys = existingAllowances.stream()
                .map(key -> Pair.of(key.tokenId(), key.spenderId()))
                .collect(Collectors.toSet());
        final var newKeys = new HashSet>();
        for (final var key : newAllowances) {
            final var newKey = Pair.of(key.tokenId(), key.spender());
            if (!existingKeys.contains(newKey) && !newKeys.contains(newKey)) {
                newKeys.add(newKey);
                counter++;
            }
        }
        return counter;
    }

    /**
     * Gets the number of new approveForAllNftAllowances that will be added to state from the transaction.
     * @param newAllowances the list of new nft allowances
     * @param existingAllowances the list of existing nft allowances
     * @return the number of new approveForAllNftAllowances that will be added to state from the transaction
     */
    private int getChangedNftKeys(
            final List newAllowances, final List existingAllowances) {
        int counter = 0;
        final var existingKeys = existingAllowances.stream()
                .map(key -> Pair.of(key.tokenId(), key.spenderId()))
                .collect(Collectors.toSet());
        final var newKeys = new HashSet>();
        for (final var key : newAllowances) {
            final var newKey = Pair.of(key.tokenId(), key.spender());
            if (!existingKeys.contains(newKey) && !newKeys.contains(newKey)) {
                newKeys.add(newKey);
                counter++;
            }
        }
        return counter;
    }

    /**
     * Counts the number of serials in the list of nft allowances. It doesn't consider duplicates.
     * @param nftAllowancesList the list of nft allowances
     * @return the number of serials
     */
    private int countSerials(final List nftAllowancesList) {
        int totalSerials = 0;
        for (var allowance : nftAllowancesList) {
            totalSerials += allowance.serialNumbers().size();
        }
        return totalSerials;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy