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

com.wavesplatform.state.diffs.FeeValidation.scala Maven / Gradle / Ivy

The newest version!
package com.wavesplatform.state.diffs

import cats.data.Chain
import cats.syntax.foldable.*
import com.wavesplatform.features.BlockchainFeatures
import com.wavesplatform.lang.ValidationError
import com.wavesplatform.state.*
import com.wavesplatform.transaction.*
import com.wavesplatform.transaction.Asset.{IssuedAsset, Waves}
import com.wavesplatform.transaction.EthereumTransaction.Transfer
import com.wavesplatform.transaction.TxValidationError.*
import com.wavesplatform.transaction.assets.*
import com.wavesplatform.transaction.assets.exchange.*
import com.wavesplatform.transaction.smart.*
import com.wavesplatform.transaction.transfer.*
import com.wavesplatform.transaction.validation.impl.DataTxValidator

object FeeValidation {

  case class FeeDetails(asset: Asset, requirements: Chain[String], minFeeInAsset: Long, minFeeInWaves: Long)

  val ScriptExtraFee    = 400000L
  val FeeUnit           = 100000
  val NFTMultiplier     = 0.001
  val BlockV5Multiplier = 0.001

  val FeeConstants: Map[TransactionType.TransactionType, Long] = Map(
    TransactionType.Genesis          -> 0,
    TransactionType.Payment          -> 1,
    TransactionType.Issue            -> 1000,
    TransactionType.Reissue          -> 1000,
    TransactionType.Burn             -> 1,
    TransactionType.Transfer         -> 1,
    TransactionType.MassTransfer     -> 1,
    TransactionType.Lease            -> 1,
    TransactionType.LeaseCancel      -> 1,
    TransactionType.Exchange         -> 3,
    TransactionType.CreateAlias      -> 1,
    TransactionType.Data             -> 1,
    TransactionType.SetScript        -> 10,
    TransactionType.SponsorFee       -> 1000,
    TransactionType.SetAssetScript   -> (1000 - 4),
    TransactionType.InvokeScript     -> 5,
    TransactionType.UpdateAssetInfo  -> 1,
    TransactionType.Ethereum         -> 1,
    TransactionType.InvokeExpression -> 10
  )

  def apply(blockchain: Blockchain, tx: Transaction): Either[ValidationError, Unit] = {
    if (blockchain.height >= Sponsorship.sponsoredFeesSwitchHeight(blockchain)) {
      for {
        feeDetails <- getMinFee(blockchain, tx)
        _ <- Either.cond(
          feeDetails.minFeeInAsset <= tx.fee,
          (),
          notEnoughFeeError(tx.tpe, feeDetails, tx.fee)
        )
      } yield ()
    } else {
      Either.cond(tx.fee > 0 || !tx.isInstanceOf[Authorized], (), GenericError(s"Fee must be positive."))
    }
  }

  private def notEnoughFeeError(txType: TransactionType.TransactionType, feeDetails: FeeDetails, feeAmount: Long): ValidationError = {
    val actualFee   = s"$feeAmount in ${feeDetails.asset.fold("WAVES")(_.id.toString)}"
    val requiredFee = s"${feeDetails.minFeeInWaves} WAVES${feeDetails.asset.fold("")(id => s" or ${feeDetails.minFeeInAsset} ${id.id.toString}")}"

    val errorMessage = s"Fee for ${txType.transactionName} ($actualFee) does not exceed minimal value of $requiredFee."

    GenericError((if (feeDetails.requirements.nonEmpty) (feeDetails.requirements mkString_ ". ") ++ ". " else "") ++ errorMessage)
  }

  private case class FeeInfo(assetInfo: Option[(IssuedAsset, AssetDescription)], requirements: Chain[String], wavesFee: Long)

  private def feeInUnits(blockchain: Blockchain, tx: Transaction): Either[ValidationError, Long] = {
    FeeConstants
      .get(tx.tpe)
      .map { baseFee =>
        tx match {
          case tx: MassTransferTransaction =>
            baseFee + (tx.transfers.size + 1) / 2
          case tx: DataTransaction =>
            val payloadLength =
              if (blockchain.isFeatureActivated(BlockchainFeatures.RideV6)) DataTxValidator.realUserPayloadSize(tx.data)
              else if (PBSince.affects(tx)) tx.protoDataPayload.length
              else if (blockchain.isFeatureActivated(BlockchainFeatures.SmartAccounts)) tx.bodyBytes().length
              else tx.bytes().length

            baseFee + (payloadLength - 1) / 1024
          case itx: IssueTransaction =>
            val multiplier = if (blockchain.isNFT(itx)) NFTMultiplier else 1

            (baseFee * multiplier).toLong
          case _: ReissueTransaction =>
            val multiplier = if (blockchain.isFeatureActivated(BlockchainFeatures.BlockV5)) BlockV5Multiplier else 1
            (baseFee * multiplier).toLong
          case _: SponsorFeeTransaction =>
            val multiplier = if (blockchain.isFeatureActivated(BlockchainFeatures.BlockV5)) BlockV5Multiplier else 1
            (baseFee * multiplier).toLong
          case et: EthereumTransaction =>
            et.payload match {
              case _: EthereumTransaction.Transfer   => 1
              case _: EthereumTransaction.Invocation => 5
            }

          case ss: SetScriptTransaction if blockchain.isFeatureActivated(BlockchainFeatures.RideV6) =>
            ss.script.fold(1) { script =>
              val scriptSize = script.bytes().size
              val kbs        = scriptSize / 1024
              if (scriptSize > 0 && scriptSize % 1024 == 0) kbs else kbs + 1
            }

          case _ => baseFee
        }
      }
      .toRight(UnsupportedTransactionType)
  }

  private def feeAfterSponsorship(txAsset: Asset, blockchain: Blockchain, tx: Transaction): Either[ValidationError, FeeInfo] = {
    if (blockchain.height < Sponsorship.sponsoredFeesSwitchHeight(blockchain)) {
      // This could be true for private blockchains
      feeInUnits(blockchain, tx).map(x => FeeInfo(None, Chain.empty, x * FeeUnit))
    } else {
      for {
        feeInUnits <- feeInUnits(blockchain, tx)
        r <- txAsset match {
          case Waves => Right(FeeInfo(None, Chain.empty, feeInUnits * FeeUnit))
          case assetId @ IssuedAsset(_) =>
            for {
              assetInfo <- blockchain
                .assetDescription(assetId)
                .toRight(GenericError(s"Asset ${assetId.id.toString} does not exist, cannot be used to pay fees"))
              wavesFee <- Either.cond(
                assetInfo.sponsorship > 0,
                feeInUnits * FeeUnit,
                GenericError(s"Asset ${assetId.id.toString} is not sponsored, cannot be used to pay fees")
              )
            } yield FeeInfo(Some((assetId, assetInfo)), Chain.empty, wavesFee)
        }
      } yield r
    }
  }

  private def feeAfterSmartTokens(blockchain: Blockchain, tx: Transaction)(inputFee: FeeInfo): FeeInfo = {
    val FeeInfo(feeAssetInfo, requirements, feeAmount) = inputFee

    val tokenIsSmart: Boolean =
      feeAssetInfo
        .map(_._1)
        .flatMap(blockchain.assetDescription)
        .exists(_.script.isDefined)

    val assetsCount = tx match {
      case _: InvokeScriptTransaction =>
        if (blockchain.isFeatureActivated(BlockchainFeatures.SynchronousCalls)) 0 else tx.smartAssets(blockchain).size
      case tx: ExchangeTransaction =>
        tx.smartAssets(blockchain).size /* *3 if we decide to check orders and transaction */
      case EthereumTransaction(t: Transfer, _, _, _) =>
        t.tryResolveAsset(blockchain)
          .collectFirst { case i: IssuedAsset => blockchain.hasAssetScript(i) }
          .fold(0)(hasAsset => if (hasAsset) 1 else 0)
      case _ =>
        tx.smartAssets(blockchain).size
    }

    val finalAssetsCount =
      if (tokenIsSmart) assetsCount + 1
      else assetsCount

    val extraFee = finalAssetsCount * ScriptExtraFee

    val extraRequirements =
      if (finalAssetsCount > 0)
        Chain(s"Transaction involves $finalAssetsCount scripted assets. Requires $extraFee extra fee")
      else Chain.empty

    FeeInfo(feeAssetInfo, extraRequirements ++ requirements, feeAmount + extraFee)
  }

  private def feeAfterSmartAccounts(blockchain: Blockchain, tx: Transaction)(inputFee: FeeInfo): FeeInfo = {
    val smartAccountScriptsCount: Int = tx match {
      case _: EthereumTransaction          => 0
      case tx: Transaction with Authorized => if (blockchain.hasPaidVerifier(tx.sender.toAddress)) 1 else 0
      case _                               => 0
    }

    val extraFee = smartAccountScriptsCount * ScriptExtraFee
    val extraRequirements =
      if (smartAccountScriptsCount > 0) Chain(s"Transaction sent from smart account. Requires $extraFee extra fee")
      else Chain.empty

    val FeeInfo(feeAssetInfo, reqs, feeAmount) = inputFee

    FeeInfo(feeAssetInfo, extraRequirements ++ reqs, feeAmount + extraFee)
  }

  def getMinFee(blockchain: Blockchain, tx: Transaction): Either[ValidationError, FeeDetails] = {
    feeAfterSponsorship(tx.feeAssetId, blockchain, tx)
      .map(feeAfterSmartTokens(blockchain, tx))
      .map(feeAfterSmartAccounts(blockchain, tx))
      .map {
        case FeeInfo(Some((assetId, assetInfo)), reqs, amountInWaves) =>
          FeeDetails(assetId, reqs, Sponsorship.fromWaves(amountInWaves, assetInfo.sponsorship), amountInWaves)
        case FeeInfo(None, reqs, amountInWaves) =>
          FeeDetails(Waves, reqs, amountInWaves, amountInWaves)
      }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy