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

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

The newest version!
package com.wavesplatform.transaction

import cats.instances.either.*
import cats.syntax.functor.*
import cats.instances.vector.*
import cats.syntax.either.*
import cats.syntax.traverse.*
import com.esaulpaugh.headlong.abi.{Function, Tuple}
import com.esaulpaugh.headlong.util.FastHex
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.features.BlockchainFeatures.BlockRewardDistribution
import com.wavesplatform.lang.script.Script
import com.wavesplatform.lang.v1.FunctionHeader
import com.wavesplatform.lang.v1.compiler.Terms.{EVALUATED, FUNCTION_CALL}
import com.wavesplatform.lang.v1.compiler.Types.TypeExt
import com.wavesplatform.lang.v1.compiler.{Terms, Types}
import com.wavesplatform.lang.{Global, ValidationError}
import com.wavesplatform.state.Blockchain
import com.wavesplatform.transaction.EthABIConverter.WavesByteRepr
import com.wavesplatform.transaction.TxValidationError.GenericError
import com.wavesplatform.transaction.smart.InvokeScriptTransaction
import org.web3j.abi.TypeReference
import org.web3j.abi.datatypes.Type
import play.api.libs.json.{JsArray, JsObject, JsString, Json}

import scala.jdk.CollectionConverters.*

final case class EthABIConverter(script: Script) {
  case class FunctionArg(name: String, rideType: Types.FINAL) {
    lazy val ethType: String = EthABIConverter.ethType(rideType)

    def ethTypeRef: TypeReference[Type[?]] = TypeReference.makeTypeReference(ethType).asInstanceOf[TypeReference[Type[?]]]
  }

  case class FunctionRef(name: String, args: Seq[FunctionArg]) {

    def decodeArgs(
        data: String,
        blockchain: Blockchain
    ): Either[ValidationError, (List[EVALUATED], Seq[InvokeScriptTransaction.Payment])] = {
      val arr   = FastHex.decode(data)
      val func  = new Function(ethSignature)
      val tuple: Tuple = func.decodeCall(arr)

      (tuple: java.lang.Iterable[AnyRef]).asScala.toList
        .zip(args.map(_.rideType) :+ EthABIConverter.PaymentListType)
        .traverse { case (ethArg, rideT) => EthABIConverter.toRideValue(ethArg, rideT) }
        .flatMap(checkLen(func, tuple, arr.length, blockchain).as(_))
        .flatMap { alldecodedArgs =>
          (alldecodedArgs.last match {
            case Terms.ARR(xs) =>
              xs.toVector.traverse {
                case Terms.ARR(fields) =>
                  fields match {
                    case Seq(Terms.CONST_BYTESTR(assetId), Terms.CONST_LONG(amount)) =>
                      Right(
                        InvokeScriptTransaction.Payment(
                          amount,
                          assetId match {
                            case WavesByteRepr => Asset.Waves
                            case assetId       => Asset.IssuedAsset(assetId)
                          }
                        )
                      )

                    case other => Left(GenericError(s"decodeArgs: unexpected term in payment: $other"))
                  }
                case other => Left(GenericError(s"decodeArgs: unexpected term in payment: $other"))
              }

            case _ => Right(Nil)
          }).map(ps => (alldecodedArgs.init, ps))
        }
    }

    lazy val ethSignature: String = {
      val argTypes = args.map(_.rideType).map(EthABIConverter.ethFuncSignatureTypeName) :+ EthABIConverter.PaymentArgSignature
      s"$name(${argTypes.mkString(",")})"
    }

    lazy val ethMethodId: String = EthABIConverter.buildMethodId(ethSignature)

    def checkLen(func: Function, tuple: Tuple, len: Int, blockchain: Blockchain): Either[GenericError, Unit] = {
      val cls    = Class.forName("com.esaulpaugh.headlong.abi.TupleType")
      val method = cls.getDeclaredMethod("byteLength", classOf[Tuple])
      method.setAccessible(true)
      Either.cond(
        !blockchain
          .isFeatureActivated(BlockRewardDistribution) || method.invoke(func.getInputs, tuple).asInstanceOf[Int] == len - Function.SELECTOR_LEN,
        (),
        GenericError("Redundant bytes were found in Ethereum Invoke")
      )
    }
  }

  private[this] lazy val funcsWithTypes =
    Global
      .dAppFuncTypes(script)
      .map { signatures =>
        val filtered = signatures.argsWithFuncName.filter { case (_, args) =>
          !args.exists { case (_, tpe) => tpe.containsUnion }
        }
        signatures.copy(argsWithFuncName = filtered)
      }

  private[this] def functionsWithArgs: Seq[(String, List[(String, Types.FINAL)])] = {
    funcsWithTypes match {
      case Right(signatures) => signatures.argsWithFuncName.toSeq
      case Left(_)           => Nil
    }
  }

  lazy val funcByMethodId: Map[String, FunctionRef] =
    functionsWithArgs
      .map { case (funcName, args) =>
        FunctionRef(funcName, args.map { case (name, argType) => FunctionArg(name, argType) })
      }
      .map(func => func.ethMethodId -> func)
      .toMap

  def jsonABI: JsArray =
    JsArray(functionsWithArgs.map { case (funcName, args) =>
      val inputs = args.map { case (argName, argType) =>
        Json.obj("name" -> argName) ++ EthABIConverter.ethTypeObj(argType)
      } :+ EthABIConverter.PaymentArgJson

      Json.obj(
        "name"            -> funcName,
        "type"            -> "function",
        "constant"        -> false,
        "payable"         -> false,
        "stateMutability" -> "nonpayable",
        "inputs"          -> inputs,
        "outputs"         -> JsArray.empty
      )
    })

  def decodeFunctionCall(
      data: String,
      blockchain: Blockchain
  ): Either[ValidationError, (FUNCTION_CALL, Seq[InvokeScriptTransaction.Payment])] = {
    val methodId = data.substring(0, 8)
    for {
      function        <- funcByMethodId.get("0x" + methodId).toRight[ValidationError](GenericError(s"Function not defined: $methodId"))
      argsAndPayments <- function.decodeArgs(data, blockchain)
    } yield (FUNCTION_CALL(FunctionHeader.User(function.name), argsAndPayments._1), argsAndPayments._2)
  }
}

object EthABIConverter {
  val WavesByteRepr: ByteStr      = ByteStr(new Array[Byte](32))
  val PaymentListType: Types.LIST = Types.LIST(Types.TUPLE(List(Types.BYTESTR, Types.LONG)))
  val PaymentArgSignature: String = "(bytes32,int64)[]"
  val PaymentArgJson: JsObject = Json.obj(
    "name" -> "payments",
    "type" -> "tuple[]",
    "components" -> Json.arr(
      Json.obj("name" -> "assetId", "type" -> "bytes32"),
      Json.obj("name" -> "amount", "type"  -> "int64")
    )
  )

  private def buildMethodId(str: String): String = {
    val cls    = Class.forName("org.web3j.abi.FunctionEncoder")
    val method = cls.getDeclaredMethod("buildMethodId", classOf[String])
    method.setAccessible(true)
    method.invoke(null, str).asInstanceOf[String]
  }

  def ethType(argType: Types.FINAL): String =
    (ethTypeObj(argType) \ "type").as[String]

  def ethTypeObj(argType: Types.FINAL): JsObject = {
    def t(s: String) = Json.obj("type" -> s)

    argType match {
      case Types.BOOLEAN => t("bool")
      case Types.LONG    => t("int64")
      case Types.BYTESTR => t("bytes")
      case Types.STRING  => t("string")
      case Types.LIST(innerType) =>
        val base = ethTypeObj(innerType)
        if (base.value("type").asInstanceOf[JsString].value == "tuple") {
          Json.obj(
            "type"       -> "tuple[]",
            "components" -> base.value("components")
          )
        } else {
          Json.obj("type" -> (base.value("type").as[String] + "[]"))
        }
      case Types.UNION(tpe :: Nil, _) => ethTypeObj(tpe)
      // only for payments
      case Types.TUPLE(types) =>
        t("tuple") ++ Json.obj(
          "components" -> types.map(t => Json.obj("name" -> t.name) ++ ethTypeObj(t))
        )

      case other => throw new IllegalArgumentException(s"ethTypeObj: Unexpected type: $other")
    }
  }

  def ethFuncSignatureTypeName(argType: Types.FINAL): String = argType match {
    case Types.BOOLEAN              => "bool"
    case Types.LONG                 => "int64"
    case Types.BYTESTR              => "bytes"
    case Types.STRING               => "string"
    case Types.LIST(innerType)      => s"${ethFuncSignatureTypeName(innerType)}[]"
    case Types.UNION(tpe :: Nil, _) => ethFuncSignatureTypeName(tpe)
    case Types.TUPLE(types)         => s"(${types.map(ethFuncSignatureTypeName).mkString(",")})"
    case other                      => throw new IllegalArgumentException(s"ethFuncSignatureTypeName: Unexpected type: $other")
  }

  def toRideValue(ethArg: Any, rideType: Types.FINAL): Either[ValidationError, EVALUATED] = ethArg match {
    case bool: Boolean        => Terms.CONST_BOOLEAN(bool).asRight
    case i: Int               => Terms.CONST_LONG(i).asRight
    case l: Long              => Terms.CONST_LONG(l).asRight
    case byteArr: Array[Byte] => Terms.CONST_BYTESTR(ByteStr(byteArr)).leftMap(ce => GenericError(ce.message))
    case str: String          => Terms.CONST_STRING(str).leftMap(ce => GenericError(ce.message))

    case arr: Array[?] =>
      val innerType = rideType match {
        case list: Types.LIST =>
          list.innerType
        case _ =>
          Types.ANY
      }
      arr.toVector
        .traverse(el => toRideValue(el, innerType))
        .flatMap[ValidationError, EVALUATED] { validArgs =>
          Terms.ARR(validArgs, limited = true).leftMap(ee => GenericError(ee.message))
        }

    case t: Tuple =>
      t.asScala.toVector
        .traverse(el => toRideValue(el, Types.ANY))
        .flatMap[ValidationError, EVALUATED] { validArgs =>
          Terms.ARR(validArgs, limited = true).leftMap(ee => GenericError(ee.message))
        }

    case _ => throw new UnsupportedOperationException(s"Type not supported: $ethArg")
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy