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

com.wavesplatform.transaction.EthereumTransaction.scala Maven / Gradle / Ivy

The newest version!
package com.wavesplatform.transaction

import cats.implicits.toBifunctorOps
import com.wavesplatform.account.*
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.crypto.EthereumKeyLength
import com.wavesplatform.features.BlockchainFeatures.BlockRewardDistribution
import com.wavesplatform.lang.ValidationError
import com.wavesplatform.lang.v1.ContractLimits
import com.wavesplatform.lang.v1.compiler.Terms
import com.wavesplatform.protobuf.transaction.PBTransactions
import com.wavesplatform.state.Blockchain
import com.wavesplatform.state.diffs.invoke.InvokeScriptTransactionLike
import com.wavesplatform.transaction.Asset.{IssuedAsset, Waves}
import com.wavesplatform.transaction.TransactionType.TransactionType
import com.wavesplatform.transaction.TxValidationError.GenericError
import com.wavesplatform.transaction.serialization.impl.BaseTxJson
import com.wavesplatform.transaction.smart.InvokeScriptTransaction
import com.wavesplatform.transaction.transfer.TransferTransactionLike
import com.wavesplatform.transaction.validation.impl.InvokeScriptTxValidator
import com.wavesplatform.transaction.validation.{TxConstraints, TxValidator, ValidatedV}
import com.wavesplatform.utils.EthEncoding
import monix.eval.Coeval
import org.web3j.abi.TypeDecoder
import org.web3j.abi.datatypes.Address as EthAddress
import org.web3j.abi.datatypes.generated.Uint256
import org.web3j.crypto.*
import org.web3j.crypto.Sign.SignatureData
import org.web3j.utils.Convert
import play.api.libs.json.*

import java.math.BigInteger
import scala.reflect.ClassTag

final case class EthereumTransaction(
    payload: EthereumTransaction.Payload,
    underlying: RawTransaction,
    signatureData: SignatureData,
    override val chainId: Byte
) extends Transaction(TransactionType.Ethereum)
    with Authorized
    with PBSince.V1 { self =>
  import EthereumTransaction.*

  override val bytes: Coeval[Array[Byte]] = Coeval.evalOnce(encodeTransaction(underlying, signatureData))

  override val bodyBytes: Coeval[Array[Byte]] = Coeval.evalOnce(TransactionEncoder.encode(underlying, chainId.toLong))

  override val id: Coeval[ByteStr] = Coeval.evalOnce(ByteStr(Hash.sha3(this.bytes())))

  override def assetFee: (Asset, Long) = Asset.Waves -> underlying.getGasLimit.longValueExact()

  override val timestamp: TxTimestamp = underlying.getNonce.longValueExact()

  val signerKeyBigInt: Coeval[BigInteger] = Coeval.evalOnce {
    require(signatureData != null, "empty signature data")
    val v          = BigInt(1, signatureData.getV)
    val recoveryId = if (v > 28) v - chainId * 2 - 35 else v - 27
    val sig        = new ECDSASignature(new BigInteger(1, signatureData.getR), new BigInteger(1, signatureData.getS))

    Sign.recoverFromSignature(recoveryId.intValue, sig, Hash.sha3(this.bodyBytes()))
  }

  val signerPublicKey: Coeval[PublicKey] = Coeval.evalOnce {
    val signerKey =
      org.web3j.utils.Numeric.toBytesPadded(
        signerKeyBigInt(),
        EthereumKeyLength
      )

    PublicKey(ByteStr(signerKey))
  }

  val senderAddress: Coeval[Address] = Coeval.evalOnce(signerPublicKey().toAddress(chainId))

  override val json: Coeval[JsObject] = Coeval.evalOnce(
    BaseTxJson.toJson(this) ++ Json.obj(
      "bytes"           -> EthEncoding.toHexString(bytes()),
      "sender"          -> senderAddress().toString,
      "senderPublicKey" -> signerPublicKey()
    )
  )

  override lazy val sender: PublicKey = signerPublicKey()

  def toTransferLike(a: TxPositiveAmount, r: AddressOrAlias, asset: Asset): TransferTransactionLike = new TransferTransactionLike {
    override val amount: TxPositiveAmount       = a
    override val recipient: AddressOrAlias      = r
    override val sender: PublicKey              = signerPublicKey()
    override val assetId: Asset                 = asset
    override val attachment: ByteStr            = ByteStr.empty
    override def timestamp: TxTimestamp         = self.timestamp
    override def chainId: TxType                = self.chainId
    override def id: Coeval[ByteStr]            = self.id
    override val tpe: TransactionType           = TransactionType.Transfer
    override def assetFee: (Asset, TxTimestamp) = self.assetFee
    override def checkedAssets: Seq[IssuedAsset] = asset match {
      case i: IssuedAsset => Seq(i)
      case Asset.Waves    => Nil
    }
  }
}

object EthereumTransaction {
  sealed trait Payload

  case class Invocation(dApp: Address, hexCallData: String) extends Payload {
    def toInvokeScriptLike(tx: EthereumTransaction, blockchain: Blockchain): Either[ValidationError, InvokeScriptTransactionLike] = {
      for {
        callAndPayments <- decodeFuncCall(blockchain)
        invocation = new InvokeScriptTransactionLike {
          override def funcCall: Terms.FUNCTION_CALL                  = callAndPayments._1
          override def payments: Seq[InvokeScriptTransaction.Payment] = callAndPayments._2
          override def id: Coeval[ByteStr]                            = tx.id
          override def dApp: AddressOrAlias                           = Invocation.this.dApp
          override val sender: PublicKey                              = tx.signerPublicKey()
          override def root: InvokeScriptTransactionLike              = this
          override def assetFee: (Asset, TxTimestamp)                 = tx.assetFee
          override def timestamp: TxTimestamp                         = tx.timestamp
          override def chainId: TxVersion                             = tx.chainId
          override def checkedAssets: Seq[Asset.IssuedAsset]          = this.paymentAssets
          override val tpe: TransactionType                           = TransactionType.InvokeScript
        }
        _ <- checkPaymentsAmount(blockchain, invocation)
      } yield invocation
    }

    def decodeFuncCall(blockchain: Blockchain): Either[ValidationError, (Terms.FUNCTION_CALL, Seq[InvokeScriptTransaction.Payment])] =
      for {
        scriptInfo      <- blockchain.accountScript(dApp).toRight(GenericError(s"No script at address $dApp"))
        callAndPayments <- EthABIConverter(scriptInfo.script).decodeFunctionCall(hexCallData, blockchain)
        _ <- Either.cond(
          !blockchain.isFeatureActivated(BlockRewardDistribution) || PBTransactions
            .toPBInvokeScriptData(dApp, Some(callAndPayments._1), callAndPayments._2)
            .toByteArray
            .length <= ContractLimits.MaxInvokeScriptSizeInBytes,
          (),
          GenericError(s"Ethereum Invoke bytes length exceeds limit = ${ContractLimits.MaxInvokeScriptSizeInBytes}")
        )
      } yield callAndPayments

    private def checkPaymentsAmount(blockchain: Blockchain, invocation: InvokeScriptTransactionLike): Either[ValidationError, Unit] =
      if (blockchain.height >= blockchain.settings.functionalitySettings.ethInvokePaymentsCheckHeight)
        InvokeScriptTxValidator.checkAmounts(invocation.payments).toEither.leftMap(_.head)
      else
        Right(())
  }

  case class Transfer(tokenAddress: Option[ERC20Address], amount: Long, recipient: Address) extends Payload {
    def tryResolveAsset(blockchain: Blockchain): Either[ValidationError, Asset] =
      tokenAddress
        .fold[Either[ValidationError, Asset]](
          Right(Waves)
        )(a => blockchain.resolveERC20Address(a).toRight(GenericError(s"Can't resolve ERC20 address $a")))

    def toTransferLike(tx: EthereumTransaction, blockchain: Blockchain): Either[ValidationError, TransferTransactionLike] =
      for {
        asset  <- tryResolveAsset(blockchain)
        amount <- TxPositiveAmount(amount)(TxValidationError.NonPositiveAmount(amount, asset.maybeBase58Repr.getOrElse("waves")))
      } yield tx.toTransferLike(amount, recipient, asset)

    def checkTransferDataSize(blockchain: Blockchain, data: String): Either[GenericError, Unit] =
      Either.cond(
        !blockchain.isFeatureActivated(BlockRewardDistribution) || tokenAddress.isEmpty || EthEncoding.cleanHexPrefix(data).length == AssetDataLength,
        (),
        GenericError("Invalid asset data size for Ethereum Transfer")
      )
  }

  implicit object EthereumTransactionValidator extends TxValidator[EthereumTransaction] {
    override def validate(tx: EthereumTransaction): ValidatedV[EthereumTransaction] = TxConstraints.seq(tx)(
      TxConstraints
        .cond(tx.signatureData.getV.isEmpty || BigInt(1, tx.signatureData.getV) > 28, GenericError("Legacy transactions are not supported")),
      TxConstraints.fee(tx.underlying.getGasLimit.longValueExact()),
      TxConstraints
        .positiveOrZeroAmount((BigInt(tx.underlying.getValue) / AmountMultiplier).bigInteger.longValueExact(), "waves"),
      TxConstraints.cond(tx.underlying.getGasPrice == GasPrice, GenericError("Gas price must be 10 Gwei")),
      TxConstraints.cond(
        tx.underlying.getValue != BigInteger.ZERO || EthEncoding.cleanHexPrefix(tx.underlying.getData).nonEmpty,
        GenericError("Transaction cancellation is not supported")
      ),
      TxConstraints
        .cond(tx.underlying.getData.isEmpty || BigInt(tx.underlying.getValue) == 0, GenericError("Transaction should have either data or value")),
      tx.payload match {
        case Transfer(tokenAddress, amount, _) =>
          TxConstraints.positiveAmount(amount, tokenAddress.fold("waves")(erc20 => EthEncoding.toHexString(erc20.arr)))
        case Invocation(_, _) => TxConstraints.seq(tx)()
      }
    )
  }

  val GasPrice: BigInteger = Convert.toWei("10", Convert.Unit.GWEI).toBigInteger

  val AmountMultiplier = 10000000000L
  val AssetDataLength  = 136

  private val decodeMethod = {
    val m = classOf[TypeDecoder].getDeclaredMethod("decode", classOf[String], classOf[Int], classOf[Class[?]])
    m.setAccessible(true)
    m
  }

  private def decode[A](source: String, offset: Int)(implicit ct: ClassTag[A]): A =
    decodeMethod.invoke(null, source, offset, ct.runtimeClass.asInstanceOf[Class[A]]).asInstanceOf[A]

  private val encodeMethod = {
    val m = classOf[TransactionEncoder].getDeclaredMethod("encode", classOf[RawTransaction], classOf[SignatureData])
    m.setAccessible(true)
    m
  }

  private def encodeTransaction(tx: RawTransaction, signatureData: SignatureData): Array[Byte] =
    encodeMethod.invoke(null, tx, signatureData).asInstanceOf[Array[Byte]]

  def apply(bytes: Array[Byte]): Either[ValidationError, EthereumTransaction] =
    apply(TransactionDecoder.decode(EthEncoding.toHexString(bytes)).asInstanceOf[SignedRawTransaction])

  val ERC20TransferPrefix: String = "a9059cbb"

  def extractPayload(underlying: RawTransaction, chainId: Byte): Payload = {
    val hexData               = EthEncoding.cleanHexPrefix(underlying.getData)
    val recipientBytes        = ByteStr(EthEncoding.toBytes(underlying.getTo))
    lazy val recipientAddress = Address(recipientBytes.arr, chainId)

    hexData match {
      // Waves transfer
      case "" =>
        val amount = BigInt(underlying.getValue) / AmountMultiplier
        Transfer(
          None,
          amount.bigInteger.longValueExact(),
          recipientAddress
        )

      // Asset transfer
      case transferCall if transferCall.startsWith(ERC20TransferPrefix) =>
        val recipient = decode[EthAddress](transferCall, 8)
        val amount    = decode[Uint256](transferCall, 72)
        Transfer(
          Some(ERC20Address(recipientBytes)),
          amount.getValue.longValueExact(),
          Address(EthEncoding.toBytes(recipient.toString), chainId)
        )

      // Script invocation
      case customCall =>
        Invocation(recipientAddress, customCall)
    }
  }

  def apply(underlying: RawTransaction): Either[ValidationError, EthereumTransaction] =
    new EthereumTransaction(
      extractPayload(underlying, AddressScheme.current.chainId),
      underlying,
      new SignatureData(Array.emptyByteArray, Array.emptyByteArray, Array.emptyByteArray),
      AddressScheme.current.chainId
    ).validatedEither

  def apply(underlying: SignedRawTransaction): Either[ValidationError, EthereumTransaction] = {
    val chainId = Option(underlying.getChainId).fold(AddressScheme.current.chainId)(_.toByte)
    new EthereumTransaction(
      extractPayload(underlying, chainId),
      underlying,
      underlying.getSignatureData,
      chainId
    ).validatedEither
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy