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

com.hedera.node.app.service.token.impl.api.TokenServiceApiImpl 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.api;

import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_DELETED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_EXPIRED_AND_PENDING_REMOVAL;
import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_IS_TREASURY;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_ACCOUNT_BALANCE;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSFER_ACCOUNT_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.TRANSACTION_REQUIRES_ZERO_TOKEN_BALANCES;
import static com.hedera.node.app.service.token.api.TokenServiceApi.FreeAliasOnDeletion.YES;
import static com.hedera.node.app.spi.key.KeyUtils.IMMUTABILITY_SENTINEL_KEY;
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.ContractID;
import com.hedera.hapi.node.base.Key;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.hapi.node.token.CryptoTransferTransactionBody;
import com.hedera.node.app.service.token.ReadableAccountStore;
import com.hedera.node.app.service.token.api.ContractChangeSummary;
import com.hedera.node.app.service.token.api.FeeRecordBuilder;
import com.hedera.node.app.service.token.api.TokenServiceApi;
import com.hedera.node.app.service.token.impl.WritableAccountStore;
import com.hedera.node.app.service.token.impl.validators.StakingValidator;
import com.hedera.node.app.spi.fees.Fees;
import com.hedera.node.app.spi.info.NetworkInfo;
import com.hedera.node.app.spi.metrics.StoreMetricsService;
import com.hedera.node.app.spi.validation.EntityType;
import com.hedera.node.app.spi.validation.ExpiryValidator;
import com.hedera.node.app.spi.workflows.record.DeleteCapableTransactionRecordBuilder;
import com.hedera.node.config.data.AccountsConfig;
import com.hedera.node.config.data.HederaConfig;
import com.hedera.node.config.data.LedgerConfig;
import com.hedera.node.config.data.StakingConfig;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import com.swirlds.config.api.Configuration;
import com.swirlds.state.spi.WritableStates;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.function.Predicate;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * Implements {@link TokenServiceApi} via {@link WritableAccountStore} calls.
 */
public class TokenServiceApiImpl implements TokenServiceApi {
    private static final Logger logger = LogManager.getLogger(TokenServiceApiImpl.class);
    private static final Key STANDIN_CONTRACT_KEY =
            Key.newBuilder().contractID(ContractID.newBuilder().contractNum(0)).build();

    private final StakingValidator stakingValidator;
    private final WritableAccountStore accountStore;
    private final AccountID fundingAccountID;
    private final AccountID stakingRewardAccountID;
    private final AccountID nodeRewardAccountID;
    private final StakingConfig stakingConfig;
    private final Predicate customFeeTest;

    /**
     * Constructs a {@link TokenServiceApiImpl}
     * @param config the configuration
     * @param storeMetricsService the store metrics service
     * @param stakingValidator the staking validator
     * @param writableStates the writable states
     * @param customFeeTest a predicate for determining if a transfer has custom fees
     */
    public TokenServiceApiImpl(
            @NonNull final Configuration config,
            @NonNull final StoreMetricsService storeMetricsService,
            @NonNull final StakingValidator stakingValidator,
            @NonNull final WritableStates writableStates,
            @NonNull final Predicate customFeeTest) {
        this.customFeeTest = customFeeTest;
        requireNonNull(config);
        this.accountStore = new WritableAccountStore(writableStates, config, storeMetricsService);
        this.stakingValidator = requireNonNull(stakingValidator);

        // Determine whether staking is enabled
        stakingConfig = config.getConfigData(StakingConfig.class);

        // Compute the account ID's for funding (normally 0.0.98) and staking rewards (normally 0.0.800).
        final var hederaConfig = config.getConfigData(HederaConfig.class);
        fundingAccountID = AccountID.newBuilder()
                .shardNum(hederaConfig.shard())
                .realmNum(hederaConfig.realm())
                .accountNum(config.getConfigData(LedgerConfig.class).fundingAccount())
                .build();
        stakingRewardAccountID = AccountID.newBuilder()
                .shardNum(hederaConfig.shard())
                .realmNum(hederaConfig.realm())
                .accountNum(config.getConfigData(AccountsConfig.class).stakingRewardAccount())
                .build();
        nodeRewardAccountID = AccountID.newBuilder()
                .shardNum(hederaConfig.shard())
                .realmNum(hederaConfig.realm())
                .accountNum(config.getConfigData(AccountsConfig.class).nodeRewardAccount())
                .build();
    }

    @Override
    public void assertValidStakingElectionForCreation(
            final boolean isStakingEnabled,
            final boolean hasDeclineRewardChange,
            @NonNull final String stakedIdKind,
            @Nullable final AccountID stakedAccountIdInOp,
            @Nullable final Long stakedNodeIdInOp,
            @NonNull final ReadableAccountStore accountStore,
            @NonNull final NetworkInfo networkInfo) {
        stakingValidator.validateStakedIdForCreation(
                isStakingEnabled,
                hasDeclineRewardChange,
                stakedIdKind,
                stakedAccountIdInOp,
                stakedNodeIdInOp,
                accountStore,
                networkInfo);
    }

    @Override
    public void assertValidStakingElectionForUpdate(
            final boolean isStakingEnabled,
            final boolean hasDeclineRewardChange,
            @NonNull final String stakedIdKind,
            @Nullable final AccountID stakedAccountIdInOp,
            @Nullable final Long stakedNodeIdInOp,
            @NonNull final ReadableAccountStore accountStore,
            @NonNull final NetworkInfo networkInfo) {
        stakingValidator.validateStakedIdForUpdate(
                isStakingEnabled,
                hasDeclineRewardChange,
                stakedIdKind,
                stakedAccountIdInOp,
                stakedNodeIdInOp,
                accountStore,
                networkInfo);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void markAsContract(@NonNull final AccountID accountId, @Nullable AccountID autoRenewAccountId) {
        requireNonNull(accountId);
        final var accountAsContract = requireNonNull(accountStore.get(accountId))
                .copyBuilder()
                .smartContract(true)
                .autoRenewAccountId(autoRenewAccountId)
                .build();
        accountStore.put(accountAsContract);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void finalizeHollowAccountAsContract(@NonNull final AccountID hollowAccountId) {
        requireNonNull(hollowAccountId);
        final var hollowAccount = requireNonNull(accountStore.get(hollowAccountId));
        if (!IMMUTABILITY_SENTINEL_KEY.equals(hollowAccount.keyOrThrow())) {
            throw new IllegalArgumentException(
                    "Cannot finalize non-hollow account " + hollowAccountId + " as contract");
        }
        final var accountAsContract = hollowAccount
                .copyBuilder()
                .key(STANDIN_CONTRACT_KEY)
                .smartContract(true)
                .build();
        accountStore.put(accountAsContract);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void deleteContract(@NonNull final ContractID contractId) {
        requireNonNull(contractId);

        // If the contractId cannot find a contract, then we have nothing to do here. But that would be an error
        // condition -- it really should never happen.
        final var contract = accountStore.getContractById(contractId);
        if (contract == null) {
            logger.warn("Contract {} does not exist, so cannot be deleted", contractId);
            return;
        }

        // It may be that the contract exists, but has already been deleted. In that case, there really shouldn't
        // be anything to do here. But we'll log a warning just in case, because it would indicate a very probable
        // bug somewhere. And we'll go ahead and do the cleanup anyway.
        if (contract.deleted()) {
            logger.warn("Trying to delete Contract {}, which is already deleted", contractId);
        }

        // The contract account may or may not have an alias on it. Normally they do, but if they are created using
        // the HAPI ContractCreate, they don't necessarily have an alias (the user has to choose to do so). This means
        // If there is an alias, then we need to remove it from the account store, and we need to remove the alias
        // from the contract account.
        final var evmAddress = contract.alias();
        accountStore.removeAlias(evmAddress);
        accountStore.put(contract.copyBuilder().alias(Bytes.EMPTY).deleted(true).build());

        // It may be (but should never happen) that the alias in the given contractId does not match the alias on the
        // contract account itself. This shouldn't happen because it means that somehow we were able to look up the
        // contract from the store using alias A, but then the contract we got back had alias B. Since the alias
        // cannot be changed once set, this shouldn't be possible. We will log an error and remove the alias in the
        // contract ID from the store.
        final var usedEvmAddress = contractId.evmAddressOrElse(Bytes.EMPTY);
        if (!usedEvmAddress.equals(evmAddress)) {
            logger.error(
                    "Contract {} has an alias {} different than its referencing EVM address {}",
                    contractId,
                    evmAddress,
                    usedEvmAddress);
            accountStore.removeAlias(usedEvmAddress);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void incrementParentNonce(@NonNull final ContractID parentId) {
        requireNonNull(parentId);
        final var contract = requireNonNull(accountStore.getContractById(parentId));
        accountStore.put(contract.copyBuilder()
                .ethereumNonce(contract.ethereumNonce() + 1)
                .build());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void incrementSenderNonce(@NonNull final AccountID senderId) {
        requireNonNull(senderId);
        final var sender = requireNonNull(accountStore.get(senderId));
        accountStore.put(
                sender.copyBuilder().ethereumNonce(sender.ethereumNonce() + 1).build());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setNonce(@NonNull final AccountID accountId, final long nonce) {
        requireNonNull(accountId);
        final var target = requireNonNull(accountStore.get(accountId));
        accountStore.put(target.copyBuilder().ethereumNonce(nonce).build());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void transferFromTo(@NonNull AccountID fromId, @NonNull AccountID toId, long amount) {
        if (amount < 0) {
            throw new IllegalArgumentException(
                    "Cannot transfer negative value (" + amount + " tinybars) from " + fromId + " to " + toId);
        }
        final var from = requireNonNull(accountStore.get(fromId));
        final var to = requireNonNull(accountStore.get(toId));
        if (from.tinybarBalance() < amount) {
            throw new IllegalArgumentException(
                    "Insufficient balance to transfer " + amount + " tinybars from " + fromId + " to " + toId);
        }
        if (to.tinybarBalance() + amount < 0) {
            throw new IllegalArgumentException(
                    "Overflow on transfer of " + amount + " tinybars from " + fromId + " to " + toId);
        }
        if (!from.accountIdOrThrow().equals(to.accountIdOrThrow())) {
            accountStore.put(from.copyBuilder()
                    .tinybarBalance(from.tinybarBalance() - amount)
                    .build());
            accountStore.put(to.copyBuilder()
                    .tinybarBalance(to.tinybarBalance() + amount)
                    .build());
        }
    }

    @Override
    public ContractChangeSummary summarizeContractChanges() {
        return accountStore.summarizeContractChanges();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void updateStorageMetadata(
            @NonNull final ContractID contractID, @NonNull final Bytes firstKey, final int netChangeInSlotsUsed) {
        requireNonNull(firstKey);
        requireNonNull(contractID);
        final var target = accountStore.getContractById(contractID);
        if (target == null) {
            throw new IllegalArgumentException("No contract found for ID " + contractID);
        }
        final var newNumKvPairs = target.contractKvPairsNumber() + netChangeInSlotsUsed;
        if (newNumKvPairs < 0) {
            throw new IllegalArgumentException("Cannot change # of storage slots (currently "
                    + target.contractKvPairsNumber()
                    + ") by "
                    + netChangeInSlotsUsed
                    + " for contract "
                    + contractID);
        }
        accountStore.put(target.copyBuilder()
                .firstContractStorageKey(firstKey)
                .contractKvPairsNumber(newNumKvPairs)
                .build());
    }

    @Override
    public boolean chargeNetworkFee(
            @NonNull final AccountID payerId, final long amount, @NonNull final FeeRecordBuilder rb) {
        requireNonNull(rb);
        requireNonNull(payerId);

        final var payerAccount = lookupAccount("Payer", payerId);
        final var amountToCharge = Math.min(amount, payerAccount.tinybarBalance());
        chargePayer(payerAccount, amountToCharge);
        // We may be charging for preceding child record fees, which are additive to the base fee
        rb.transactionFee(rb.transactionFee() + amountToCharge);
        distributeToNetworkFundingAccounts(amountToCharge, rb);
        return amountToCharge == amount;
    }

    @Override
    public void chargeFees(
            @NonNull AccountID payerId,
            AccountID nodeAccountId,
            @NonNull Fees fees,
            @NonNull final FeeRecordBuilder rb) {
        requireNonNull(rb);
        requireNonNull(fees);
        requireNonNull(payerId);
        requireNonNull(nodeAccountId);

        // Note: these four accounts (payer, funding, staking reward, node reward) MUST exist for the transaction to be
        // valid and for fees to be processed. If any of them do not exist, the entire transaction will fail. There is
        // no conceivable way that these accounts *should* be null at this point.
        final var payerAccount = lookupAccount("Payer", payerId);
        if (payerAccount.tinybarBalance() < fees.networkFee()) {
            throw new IllegalArgumentException(
                    "Payer %s (balance=%d) cannot afford network fee of %d, which should have been a due diligence failure"
                            .formatted(payerId, payerAccount.tinybarBalance(), fees.networkFee()));
        }
        if (fees.serviceFee() > 0 && payerAccount.tinybarBalance() < fees.totalFee()) {
            throw new IllegalArgumentException(
                    "Payer %s (balance=%d) cannot afford total fee of %d, which means service component should have been zeroed out"
                            .formatted(payerId, payerAccount.tinybarBalance(), fees.totalFee()));
        }
        // Prioritize network fee over node fee
        final long chargeableNodeFee = Math.min(fees.nodeFee(), payerAccount.tinybarBalance() - fees.networkFee());
        final long amountToCharge = fees.totalWithoutNodeFee() + chargeableNodeFee;
        final long amountToDistributeToFundingAccounts = amountToCharge - chargeableNodeFee;

        chargePayer(payerAccount, amountToCharge);
        // Record the amount charged into the record builder
        rb.transactionFee(amountToCharge);
        distributeToNetworkFundingAccounts(amountToDistributeToFundingAccounts, rb);

        if (chargeableNodeFee > 0) {
            final var nodeAccount = lookupAccount("Node account", nodeAccountId);
            accountStore.put(nodeAccount
                    .copyBuilder()
                    .tinybarBalance(nodeAccount.tinybarBalance() + chargeableNodeFee)
                    .build());
        }
    }

    @Override
    public void refundFees(@NonNull AccountID receiver, @NonNull Fees fees, @NonNull final FeeRecordBuilder rb) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public long originalKvUsageFor(@NonNull final ContractID id) {
        Account account = accountStore.getContractById(id);
        final var oldAccount = account == null ? null : accountStore.getOriginalValue(account.accountId());
        return oldAccount == null ? 0 : oldAccount.contractKvPairsNumber();
    }

    @Override
    public void updateContract(Account contract) {
        accountStore.put(contract);
    }

    /**
     * A utility method that charges (debits) the payer up to the given total fee. If the payer account doesn't exist,
     * then an exception is thrown.
     *
     * @param payerAccount the account to charge
     * @param amount the maximum amount to charge
     * @throws IllegalStateException if the payer account doesn't exist
     */
    private void chargePayer(@NonNull final Account payerAccount, final long amount) {
        if (amount > payerAccount.tinybarBalance()) {
            throw new IllegalArgumentException("Payer %s (balance=%d) cannot afford fee of %d"
                    .formatted(payerAccount, payerAccount.tinybarBalance(), amount));
        }
        final long currentBalance = payerAccount.tinybarBalance();
        accountStore.put(payerAccount
                .copyBuilder()
                .tinybarBalance(currentBalance - amount)
                .build());
    }

    /**
     * Pays the node reward account the given amount. If the node reward account doesn't exist, an exception is thrown.
     * This account *should* have been created at genesis, so it should always exist, even if staking rewards are
     * disabled.
     *
     * @param amount The amount to credit the node reward account.
     * @throws IllegalStateException if the node rewards account doesn't exist
     */
    private void payNodeRewardAccount(final long amount) {
        if (amount == 0) return;
        final var nodeAccount = lookupAccount("Node reward", nodeRewardAccountID);
        accountStore.put(nodeAccount
                .copyBuilder()
                .tinybarBalance(nodeAccount.tinybarBalance() + amount)
                .build());
    }

    /**
     * Pays the staking reward account the given amount. If the staking reward account doesn't exist, an exception is
     * thrown. This account *should* have been created at genesis, so it should always exist, even if staking rewards
     * are disabled.
     *
     * @param amount The amount to credit the staking reward account.
     * @throws IllegalStateException if the staking rewards account doesn't exist
     */
    private void payStakingRewardAccount(final long amount) {
        if (amount == 0) return;
        final var stakingAccount = lookupAccount("Staking reward", stakingRewardAccountID);
        accountStore.put(stakingAccount
                .copyBuilder()
                .tinybarBalance(stakingAccount.tinybarBalance() + amount)
                .build());
    }

    /**
     * Looks up and returns the {@link Account} with the given ID. If the account doesn't exist, an exception is thrown.
     *
     * @param logName The name of this account to use in log statements.
     * @param id The account ID to lookup
     * @return The looked up account.
     * @throws IllegalStateException if the given account doesn't exist
     */
    @NonNull
    private Account lookupAccount(String logName, AccountID id) {
        var account = accountStore.get(id);
        if (account == null) {
            throw new IllegalStateException(logName + " account " + id + " does not exist");
        }
        return account;
    }

    private record InvolvedAccounts(@NonNull Account deletedAccount, @NonNull Account obtainerAccount) {
        private InvolvedAccounts {
            requireNonNull(deletedAccount);
            requireNonNull(obtainerAccount);
        }
    }

    @Override
    public boolean checkForCustomFees(@NonNull final CryptoTransferTransactionBody op) {
        return customFeeTest.test(op);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void deleteAndTransfer(
            @NonNull final AccountID deletedId,
            @NonNull final AccountID obtainerId,
            @NonNull final ExpiryValidator expiryValidator,
            @NonNull final DeleteCapableTransactionRecordBuilder recordBuilder,
            @NonNull final FreeAliasOnDeletion freeAliasOnDeletion) {
        // validate the semantics involving dynamic properties and state.
        // Gets delete and transfer accounts from state
        final var deleteAndTransferAccounts = validateSemantics(deletedId, obtainerId, expiryValidator);
        transferRemainingBalance(expiryValidator, deleteAndTransferAccounts);

        // get the account from account store that has all balance changes
        // commit the account with deleted flag set to true
        final var updatedDeleteAccount = requireNonNull(accountStore.getForModify(deletedId));
        final var builder = updatedDeleteAccount.copyBuilder().deleted(true);
        if (freeAliasOnDeletion == YES) {
            accountStore.removeAlias(updatedDeleteAccount.alias());
            builder.alias(Bytes.EMPTY);
        }
        accountStore.put(builder.build());

        // add the transfer account for this deleted account to record builder.
        // This is needed while computing staking rewards. In the future it will also be added
        // to the transaction record exported to mirror node.
        recordBuilder.addBeneficiaryForDeletedAccount(deletedId, obtainerId);
    }

    private InvolvedAccounts validateSemantics(
            @NonNull final AccountID deletedId,
            @NonNull final AccountID obtainerId,
            @NonNull final ExpiryValidator expiryValidator) {
        // validate if accounts exist
        final var deletedAccount = accountStore.get(deletedId);
        validateTrue(deletedAccount != null, INVALID_ACCOUNT_ID);
        final var transferAccount = accountStore.get(obtainerId);
        validateTrue(transferAccount != null, INVALID_TRANSFER_ACCOUNT_ID);
        // if the account is treasury for any other token, it can't be deleted
        validateFalse(deletedAccount.numberTreasuryTitles() > 0, ACCOUNT_IS_TREASURY);
        // checks if accounts are detached
        final var isExpired = areAccountsDetached(deletedAccount, transferAccount, expiryValidator);
        validateFalse(isExpired, ACCOUNT_EXPIRED_AND_PENDING_REMOVAL);
        // An account can't be deleted if there are any tokens associated with this account
        validateTrue(deletedAccount.numberPositiveBalances() == 0, TRANSACTION_REQUIRES_ZERO_TOKEN_BALANCES);
        return new InvolvedAccounts(deletedAccount, transferAccount);
    }

    private void transferRemainingBalance(
            @NonNull final ExpiryValidator expiryValidator, @NonNull final InvolvedAccounts involvedAccounts) {
        final var fromAccount = involvedAccounts.deletedAccount();
        final var toAccount = involvedAccounts.obtainerAccount();
        final long newFromBalance = computeNewBalance(expiryValidator, fromAccount, -1 * fromAccount.tinybarBalance());
        final long newToBalance = computeNewBalance(expiryValidator, toAccount, fromAccount.tinybarBalance());
        accountStore.put(
                fromAccount.copyBuilder().tinybarBalance(newFromBalance).build());
        accountStore.put(toAccount.copyBuilder().tinybarBalance(newToBalance).build());
    }

    private long computeNewBalance(
            final ExpiryValidator expiryValidator, final Account account, final long adjustment) {
        validateTrue(!account.deleted(), ACCOUNT_DELETED);
        validateTrue(
                !expiryValidator.isDetached(
                        EntityType.ACCOUNT, account.expiredAndPendingRemoval(), account.tinybarBalance()),
                ACCOUNT_EXPIRED_AND_PENDING_REMOVAL);
        final long balance = account.tinybarBalance();
        validateTrue(balance + adjustment >= 0, INSUFFICIENT_ACCOUNT_BALANCE);
        return balance + adjustment;
    }

    private boolean areAccountsDetached(
            @NonNull Account deleteAccount,
            @NonNull Account transferAccount,
            @NonNull final ExpiryValidator expiryValidator) {
        return expiryValidator.isDetached(
                        getEntityType(deleteAccount),
                        deleteAccount.expiredAndPendingRemoval(),
                        deleteAccount.tinybarBalance())
                || expiryValidator.isDetached(
                        getEntityType(transferAccount),
                        transferAccount.expiredAndPendingRemoval(),
                        transferAccount.tinybarBalance());
    }

    private EntityType getEntityType(@NonNull final Account account) {
        return account.smartContract() ? EntityType.CONTRACT : EntityType.ACCOUNT;
    }

    private void distributeToNetworkFundingAccounts(final long amount, @NonNull final FeeRecordBuilder rb) {
        // We may have a rounding error, so we will first remove the node and staking rewards from the total, and then
        // whatever is left over goes to the funding account.
        long balance = amount;

        // We only pay node and staking rewards if the feature is enabled
        if (stakingConfig.isEnabled()) {
            final long nodeReward = (stakingConfig.feesNodeRewardPercentage() * amount) / 100;
            balance -= nodeReward;
            payNodeRewardAccount(nodeReward);

            final long stakingReward = (stakingConfig.feesStakingRewardPercentage() * amount) / 100;
            balance -= stakingReward;
            payStakingRewardAccount(stakingReward);
        }

        // Whatever is left over goes to the funding account
        final var fundingAccount = lookupAccount("Funding", fundingAccountID);
        accountStore.put(fundingAccount
                .copyBuilder()
                .tinybarBalance(fundingAccount.tinybarBalance() + balance)
                .build());
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy