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

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

There is a newer version: 0.56.6
Show newest version
/*
 * Copyright (C) 2023-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.ACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN;
import static com.hedera.hapi.node.base.ResponseCodeEnum.FAIL_INVALID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_TOKEN_BALANCE;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_MINT_AMOUNT;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TREASURY_ACCOUNT_FOR_TOKEN;
import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.NO_REMAINING_AUTOMATIC_ASSOCIATIONS;
import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKENS_PER_ACCOUNT_LIMIT_EXCEEDED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT;
import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_MAX_SUPPLY_REACHED;
import static com.hedera.node.app.spi.workflows.HandleException.validateFalse;
import static com.hedera.node.app.spi.workflows.HandleException.validateTrue;
import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.ResponseCodeEnum;
import com.hedera.hapi.node.base.TokenAssociation;
import com.hedera.hapi.node.base.TokenID;
import com.hedera.hapi.node.base.TokenSupplyType;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.hapi.node.state.token.Token;
import com.hedera.hapi.node.state.token.TokenRelation;
import com.hedera.hapi.node.token.TokenUpdateTransactionBody;
import com.hedera.node.app.service.token.impl.WritableAccountStore;
import com.hedera.node.app.service.token.impl.WritableTokenRelationStore;
import com.hedera.node.app.service.token.impl.WritableTokenStore;
import com.hedera.node.app.service.token.impl.util.TokenKey;
import com.hedera.node.config.data.EntitiesConfig;
import com.hedera.node.config.data.TokensConfig;
import com.swirlds.config.api.Configuration;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * Provides common functionality for token handlers.
 */
public class BaseTokenHandler {
    private static final Logger log = LogManager.getLogger(BaseTokenHandler.class);
    /**
     * The set of token keys that are not admin keys.
     */
    protected static final Set NON_ADMIN_TOKEN_KEYS = EnumSet.complementOf(EnumSet.of(TokenKey.ADMIN_KEY));
    /**
     * The set of all token keys.
     */
    protected static final Set TOKEN_KEYS = EnumSet.allOf(TokenKey.class);

    /**
     * Mints fungible tokens. This method is called in both token create and mint.
     * @param token the new or existing token to mint
     * @param treasuryRel the treasury relation for the token
     * @param amount the amount to mint
     * @param accountStore the account store
     * @param tokenStore the token store
     * @param tokenRelationStore the token relation store
     */
    protected long mintFungible(
            @NonNull final Token token,
            @NonNull final TokenRelation treasuryRel,
            final long amount,
            @NonNull final WritableAccountStore accountStore,
            @NonNull final WritableTokenStore tokenStore,
            @NonNull final WritableTokenRelationStore tokenRelationStore) {
        requireNonNull(token);
        requireNonNull(treasuryRel);

        return changeSupply(
                token, treasuryRel, +amount, INVALID_TOKEN_MINT_AMOUNT, accountStore, tokenStore, tokenRelationStore);
    }

    /**
     * Since token mint and token burn change the supply on the token and treasury account,
     * this method is used to change the supply.
     *
     * 

* Note: This method assumes the given token has a non-null supply key! * * @param token the token that is minted or burned * @param treasuryRel the treasury relation for the token * @param amount the amount to mint or burn * @param invalidSupplyCode the invalid supply code to use if the supply is invalid * @param accountStore the account store * @param tokenStore the token store * @param tokenRelationStore the token relation store */ protected long changeSupply( @NonNull final Token token, @NonNull final TokenRelation treasuryRel, final long amount, @NonNull final ResponseCodeEnum invalidSupplyCode, @NonNull final WritableAccountStore accountStore, @NonNull final WritableTokenStore tokenStore, @NonNull final WritableTokenRelationStore tokenRelationStore) { requireNonNull(token); requireNonNull(treasuryRel); requireNonNull(invalidSupplyCode); validateTrue( treasuryRel.accountId().equals(token.treasuryAccountId()) && token.tokenId().equals(treasuryRel.tokenId()), FAIL_INVALID); final long newTotalSupply = token.totalSupply() + amount; // validate that the new total supply is not negative after mint or burn or wipe // FUTURE - All these checks that return FAIL_INVALID probably should end up in a // finalize method in token service to validate everything before we commit validateTrue(newTotalSupply >= 0, invalidSupplyCode); if (token.supplyType() == TokenSupplyType.FINITE) { validateTrue(token.maxSupply() >= newTotalSupply, TOKEN_MAX_SUPPLY_REACHED); } final var treasuryAccount = accountStore.get(treasuryRel.accountId()); validateTrue(treasuryAccount != null, INVALID_TREASURY_ACCOUNT_FOR_TOKEN); final long newTreasuryBalance = treasuryRel.balance() + amount; validateTrue(newTreasuryBalance >= 0, INSUFFICIENT_TOKEN_BALANCE); // copy the token, treasury account and treasury relation final var copyTreasuryAccount = treasuryAccount.copyBuilder(); final var copyToken = token.copyBuilder(); final var copyTreasuryRel = treasuryRel.copyBuilder(); if (treasuryRel.balance() == 0 && amount > 0) { // On an account positive balances are incremented for newly added tokens. // If treasury relation did mint any for this token till now, only then increment // total positive balances on treasury account. copyTreasuryAccount.numberPositiveBalances(treasuryAccount.numberPositiveBalances() + 1); } else if (newTreasuryBalance == 0 && amount < 0) { // On an account positive balances are decremented for burning tokens completely. // If treasury relation did not burn any for this token till now or if this burn makes the balance to 0, // only then decrement total positive balances on treasury account. copyTreasuryAccount.numberPositiveBalances(treasuryAccount.numberPositiveBalances() - 1); } // since we have either minted or burned tokens, we need to update the total supply copyToken.totalSupply(newTotalSupply); copyTreasuryRel.balance(newTreasuryBalance); // put the changed token, treasury account and treasury relation accountStore.put(copyTreasuryAccount.build()); tokenStore.put(copyToken.build()); tokenRelationStore.put(copyTreasuryRel.build()); return newTotalSupply; } /** * Creates {@link TokenRelation} object for each token association to account and links tokens to the account. * This is used in both token associate logic and also token create logic * @param account the account to link the tokens to * @param tokens the tokens to link to the account * @param accountStore the account store * @param tokenRelStore the token relation store */ protected void createAndLinkTokenRels( @NonNull final Account account, @NonNull final List tokens, @NonNull final WritableAccountStore accountStore, @NonNull final WritableTokenRelationStore tokenRelStore) { // create list of token relations to be added final var newTokenRels = createTokenRelsToAccount(account, tokens); // FUTURE - We may need to return a proper error status when tokens are empty if (!newTokenRels.isEmpty()) { // Link the new token relations to the account linkTokenRels(account, newTokenRels, tokenRelStore); // Now replace the account's old head token number with the new head token number. This is // how we link the new tokenRels to the account final var firstOfNewTokenRels = newTokenRels.get(0); final var updatedAcct = account.copyBuilder() // replace the head token number with the first token number of the new tokenRels .headTokenId(firstOfNewTokenRels.tokenId()) // and also update the account's total number of token associations .numberAssociations(account.numberAssociations() + newTokenRels.size()) .build(); // Save the results accountStore.put(updatedAcct); newTokenRels.forEach(tokenRelStore::put); } } /** * Link all the new token relations created for the account together, and then link them to the account. * @param account the account to link the tokens to * @param newTokenRels the new token relations to link to the account * @param tokenRelStore the token relation store */ private void linkTokenRels( @NonNull final Account account, @NonNull final List newTokenRels, @NonNull final WritableTokenRelationStore tokenRelStore) { // Now all the NEW token relations are linked together, but they are not yet linked to the account. First, // compute where the account's current head token number should go in the linked list of tokens final var currentHeadTokenNum = account.headTokenId(); // NOTE: if currentHeadTokenNum is less than 1, it means the account isn't associated with any tokens yet, so // we'll just set the head to the first token, i.e. the first token ID list from the transaction (since the new // tokenRels are all linked, and in the order of the token IDs as they appeared in the original list) if (isValidTokenId(currentHeadTokenNum)) { // The account is already associated with some tokens, so we need to insert the new // tokenRels at the beginning of the list of existing token numbers first. We start by // retrieving the token rel object with the currentHeadTokenNum at the head of the // account final var headTokenRel = tokenRelStore.get(account.accountId(), currentHeadTokenNum); if (headTokenRel != null) { // Recreate the current head token's tokenRel, but with its previous pointer set to // the last of the new tokenRels. This links the new token rels to the rest of the // token rels connected via the old head token rel final var lastOfNewTokenRels = newTokenRels.remove(newTokenRels.size() - 1); final var headTokenAsNonHeadTokenRel = headTokenRel .copyBuilder() .previousToken(lastOfNewTokenRels.tokenId()) .build(); // the old head token rel is no longer the head // Also connect the last of the new tokenRels to the old head token rel newTokenRels.add(lastOfNewTokenRels .copyBuilder() .nextToken(headTokenAsNonHeadTokenRel.tokenId()) .build()); tokenRelStore.put(headTokenAsNonHeadTokenRel); } else { // This shouldn't happen, but if it does we'll log the error and continue with creating the token // associations AccountID accountId = account.accountId(); log.error( "Unable to get head tokenRel for account {}.{}.{}, token {}.{}.{}! " + "Linked-list relations are likely in a bad state", accountId.shardNum(), accountId.realmNum(), accountId.accountNum(), currentHeadTokenNum.shardNum(), currentHeadTokenNum.realmNum(), currentHeadTokenNum.tokenNum()); } } } /** * Creates list of {@link TokenRelation}s for each token association to account. * @param account the account to link the tokens to * @param tokens the tokens to link to the account * @return the list of token relations to be added */ private List createTokenRelsToAccount( @NonNull final Account account, @NonNull final List tokens) { final var newTokenRels = new ArrayList(); for (int i = 0; i < tokens.size(); i++) { final var token = tokens.get(i); // Link each of the new token IDs together in a doubly-linked list way by setting each // token relation's previous and next token IDs. // Compute the previous and next token IDs. TokenID prevTokenId = null; TokenID nextTokenId = null; if (i - 1 >= 0) { // if there is a previous token prevTokenId = Optional.ofNullable(tokens.get(i - 1)) .map(Token::tokenId) .orElse(null); } if (i + 1 < tokens.size()) { // if there is a next token nextTokenId = Optional.ofNullable(tokens.get(i + 1)) .map(Token::tokenId) .orElse(null); } // Create the new token relation boolean isTreasuryAccount = Objects.equals(token.treasuryAccountId(), account.accountId()); final var isFrozen = token.hasFreezeKey() && token.accountsFrozenByDefault() && !isTreasuryAccount; final var kycGranted = !token.hasKycKey() || isTreasuryAccount; final var newTokenRel = new TokenRelation( token.tokenId(), account.accountId(), 0, isFrozen, kycGranted, false, prevTokenId, nextTokenId); newTokenRels.add(newTokenRel); } return newTokenRels; } /** * Creates a new {@link TokenRelation} with the account and token. This is called when there is * no association yet, but have open slots for maxAutoAssociations on the account. * @param account the account to link the tokens to * @param token the token to link to the account * @param accountStore the account store * @param tokenRelStore the token relation store * @param config the configuration * @return the new token relation added */ protected TokenRelation autoAssociate( @NonNull final Account account, @NonNull final Token token, @NonNull final WritableAccountStore accountStore, @NonNull final WritableTokenRelationStore tokenRelStore, @NonNull final Configuration config) { final var tokensConfig = config.getConfigData(TokensConfig.class); final var entitiesConfig = config.getConfigData(EntitiesConfig.class); final var accountId = account.accountIdOrThrow(); final var tokenId = token.tokenIdOrThrow(); // If token is already associated, no need to associate again validateTrue(tokenRelStore.get(accountId, tokenId) == null, TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT); validateTrue( tokenRelStore.sizeOfState() + 1 < tokensConfig.maxAggregateRels(), MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED); // Check is number of used associations is less than maxAutoAssociations final var numAssociations = account.numberAssociations(); validateFalse( entitiesConfig.limitTokenAssociations() && numAssociations >= tokensConfig.maxPerAccount(), TOKENS_PER_ACCOUNT_LIMIT_EXCEEDED); final var maxAutoAssociations = account.maxAutoAssociations(); final var usedAutoAssociations = account.usedAutoAssociations(); validateFalse(usedAutoAssociations >= maxAutoAssociations, NO_REMAINING_AUTOMATIC_ASSOCIATIONS); // Create new token relation and commit to store final var newTokenRel = TokenRelation.newBuilder() .tokenId(tokenId) .accountId(accountId) .automaticAssociation(true) .kycGranted(!token.hasKycKey()) .frozen(token.hasFreezeKey() && token.accountsFrozenByDefault()) .previousToken((TokenID) null) .nextToken(account.headTokenId()) .build(); final var copyAccount = account.copyBuilder() .numberAssociations(numAssociations + 1) .usedAutoAssociations(usedAutoAssociations + 1) .headTokenId(tokenId) .build(); final var existingFirstTokenId = account.headTokenId(); if (existingFirstTokenId != null) { final var existingFirstTokenRel = tokenRelStore.get(accountId, existingFirstTokenId); if (existingFirstTokenRel != null) { tokenRelStore.put(existingFirstTokenRel .copyBuilder() .previousToken(tokenId) .build()); } } accountStore.put(copyAccount); tokenRelStore.put(newTokenRel); return newTokenRel; } /** * Adjust fungible balances for the given token relationship and account by the given adjustment. * NOTE: This updates account's numOfPositiveBalances and puts to modifications on state. * @param tokenRel token relationship * @param account account to be adjusted * @param adjustment adjustment to be made * @param tokenRelStore token relationship store * @param accountStore account store */ protected void adjustBalance( @NonNull final TokenRelation tokenRel, @NonNull final Account account, final long adjustment, @NonNull final WritableTokenRelationStore tokenRelStore, @NonNull final WritableAccountStore accountStore) { requireNonNull(tokenRel); requireNonNull(account); requireNonNull(tokenRelStore); requireNonNull(accountStore); final var originalBalance = tokenRel.balance(); final var newBalance = originalBalance + adjustment; validateTrue(newBalance >= 0, INSUFFICIENT_TOKEN_BALANCE); final var copyRel = tokenRel.copyBuilder(); tokenRelStore.put(copyRel.balance(newBalance).build()); var numPositiveBalances = account.numberPositiveBalances(); // If the original balance is zero, then the receiving account's numPositiveBalances has to // be increased // and if the newBalance is zero, then the sending account's numPositiveBalances has to be // decreased if (newBalance == 0 && adjustment < 0) { numPositiveBalances--; } else if (originalBalance == 0 && adjustment > 0) { numPositiveBalances++; } final var copyAccount = account.copyBuilder(); accountStore.put(copyAccount.numberPositiveBalances(numPositiveBalances).build()); } protected void validateNotFrozenAndKycOnRelation(@NonNull final TokenRelation rel) { validateTrue(!rel.frozen(), ResponseCodeEnum.ACCOUNT_FROZEN_FOR_TOKEN); validateTrue(rel.kycGranted(), ACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN); } /* ------------------------- Helper functions ------------------------- */ /** * Checks if the given op updates any of the non-key token properties that can only be * changed given the admin key signature. * * @param op the token update op to check * @return true if it requires admin key signature */ protected static boolean updatesAdminOnlyNonKeyTokenProperty(@NonNull final TokenUpdateTransactionBody op) { return !op.symbol().isEmpty() || !op.name().isEmpty() || op.hasAutoRenewPeriod() || op.hasMemo(); } /** * Returns a new {@link TokenID} with the given number. * @param num the token number * @return the new token ID */ @NonNull public static TokenID asToken(final long num) { return TokenID.newBuilder().tokenNum(num).build(); } /** * Determines if a given token is valid * * @param tokenId the token to check * @return true if the token is valid */ public static boolean isValidTokenId(final TokenID tokenId) { if (tokenId == null) return false; return tokenId.tokenNum() > 0; } /** * Creates a new {@link TokenAssociation} with the given tokenId and accountId. * @param tokenId the token id * @param accountId the account id * @return the new token association */ public static TokenAssociation asTokenAssociation(final TokenID tokenId, final AccountID accountId) { return TokenAssociation.newBuilder() .tokenId(tokenId) .accountId(accountId) .build(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy