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

com.wavesplatform.utils.UtilApp.scala Maven / Gradle / Ivy

The newest version!
package com.wavesplatform.utils

import com.google.common.io.ByteStreams
import com.wavesplatform.account.{KeyPair, PrivateKey, PublicKey}
import com.wavesplatform.api.http.requests.*
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.common.utils.{Base58, Base64, FastBase58}
import com.wavesplatform.features.EstimatorProvider.*
import com.wavesplatform.lang.script.{Script, ScriptReader}
import com.wavesplatform.settings.{WalletSettings, WavesSettings}
import com.wavesplatform.transaction.TxValidationError.GenericError
import com.wavesplatform.transaction.smart.script.ScriptCompiler
import com.wavesplatform.transaction.{Transaction, TransactionFactory, TransactionSignOps, TransactionType}
import com.wavesplatform.wallet.Wallet
import com.wavesplatform.{Application, Version}
import play.api.libs.json.{JsObject, Json}
import scopt.OParser

import java.io.{ByteArrayInputStream, File, FileInputStream, FileOutputStream}
import java.nio.charset.StandardCharsets
import java.nio.file.{Files, Paths}
import scala.annotation.nowarn

//noinspection ScalaStyle
// TODO: Consider remove implemented methods from REST API
object UtilApp {
  object Command {
    sealed trait Mode
    case object CompileScript   extends Mode
    case object DecompileScript extends Mode
    case object SignBytes       extends Mode
    case object VerifySignature extends Mode
    case object CreateKeyPair   extends Mode
    case object Hash            extends Mode
    case object SerializeTx     extends Mode
    case object SignTx          extends Mode
    case object SignTxWithSk    extends Mode
  }

  case class CompileOptions(assetScript: Boolean = false)
  case class SignOptions(privateKey: PrivateKey = null)
  case class VerifyOptions(publicKey: PublicKey = null, signature: ByteStr = ByteStr.empty, checkWeakPk: Boolean = false)
  case class HashOptions(mode: String = "fast")
  case class SignTxOptions(signerAddress: String = "")
  case class KeyPairOptions(seedType: String = "account", nonce: Int = 0)

  sealed trait Input
  object Input {
    case object StdIn                   extends Input
    final case class File(file: String) extends Input
    final case class Str(str: String)   extends Input
  }

  sealed trait SeedType

  case class Command(
      mode: Command.Mode = null,
      configFile: Option[String] = None,
      inputData: Input = Input.StdIn,
      outputFile: Option[String] = None,
      inFormat: String = "plain",
      outFormat: String = "plain",
      compileOptions: CompileOptions = CompileOptions(),
      signOptions: SignOptions = SignOptions(),
      verifyOptions: VerifyOptions = VerifyOptions(),
      hashOptions: HashOptions = HashOptions(),
      signTxOptions: SignTxOptions = SignTxOptions(),
      keyPairOptions: KeyPairOptions = KeyPairOptions()
  )

  def main(args: Array[String]): Unit = {
    OParser.parse(commandParser, args, Command()) match {
      case Some(cmd) =>
        val settings = Application.loadApplicationConfig(cmd.configFile.map(new File(_)))
        val inBytes  = IO.readInput(cmd)
        val result = {
          val doAction = cmd.mode match {
            case Command.CompileScript   => Actions.doCompile(settings) _
            case Command.DecompileScript => Actions.doDecompile _
            case Command.SignBytes       => Actions.doSign _
            case Command.VerifySignature => Actions.doVerify _
            case Command.CreateKeyPair   => Actions.doCreateKeyPair _
            case Command.Hash            => Actions.doHash _
            case Command.SerializeTx     => Actions.doSerializeTx _
            case Command.SignTx          => Actions.doSignTx(new NodeState(cmd)) _
            case Command.SignTxWithSk    => Actions.doSignTxWithSK _
          }
          doAction(cmd, inBytes)
        }

        result match {
          case Left(value)     => System.err.println(s"Error executing command: $value")
          case Right(outBytes) => IO.writeOutput(cmd, outBytes)
        }

      case None =>
    }
  }

  private[this] lazy val commandParser = {
    import scopt.OParser

    val builder = OParser.builder[Command]
    import builder.*

    OParser.sequence(
      programName("waves util"),
      head("Waves Util", Version.VersionString),
      OParser.sequence(
        opt[String](name = "input-str")
          .abbr("is")
          .text("Literal input data")
          .action((s, c) => c.copy(inputData = Input.Str(s))),
        opt[String]('i', "input-file")
          .action((f, c) => c.copy(inputData = if (f.isEmpty || f == "-") Input.StdIn else Input.File(f)))
          .text("Input file name (- for stdin)")
          .validate {
            case fs if fs.isEmpty || fs == "-" || Files.isRegularFile(Paths.get(fs)) => success
            case fs                                                                  => failure(s"Invalid file: $fs")
          },
        opt[String]('o', "output-file")
          .action((f, c) => c.copy(outputFile = Some(f).filter(s => s != "-" && s.nonEmpty)))
          .text("Output file name (- for stdout)"),
        opt[String]("in-format")
          .abbr("fi")
          .action((f, c) => c.copy(inFormat = f))
          .text("Input data format (plain/base58/base64)")
          .validate {
            case "base64" | "base58" | "plain" => success
            case fs                            => failure(s"Invalid format: $fs")
          },
        opt[String]("out-format")
          .abbr("fo")
          .action((f, c) => c.copy(outFormat = f))
          .text("Output data format (plain/base58/base64)")
          .validate {
            case "base64" | "base58" | "plain" => success
            case fs                            => failure(s"Invalid format: $fs")
          },
        opt[String]('c', "config")
          .action((cf, c) => c.copy(configFile = Some(cf).filter(_.nonEmpty)))
          .text("Node config file path")
      ),
      cmd("script").children(
        cmd("compile")
          .action((_, c) => c.copy(mode = Command.CompileScript))
          .text("Compiles RIDE script"),
        cmd("decompile")
          .action((_, c) => c.copy(mode = Command.DecompileScript))
          .text("Decompiles binary script to RIDE code")
      ),
      cmd("hash")
        .children(
          opt[String]('m', "mode")
            .valueName("")
            .action((m, c) => c.copy(hashOptions = c.hashOptions.copy(mode = m)))
        )
        .action((_, c) => c.copy(mode = Command.Hash)),
      cmd("crypto").children(
        cmd("sign")
          .children(
            opt[String]('k', "private-key")
              .text("Private key for signing")
              .required()
              .action((s, c) => c.copy(signOptions = c.signOptions.copy(privateKey = PrivateKey(Base58.decode(s)))))
          )
          .text("Sign bytes with provided private key")
          .action((_, c) => c.copy(mode = Command.SignBytes)),
        cmd("verify")
          .children(
            opt[String]('k', "public-key")
              .text("Public key for verification")
              .required()
              .action((s, c) => c.copy(verifyOptions = c.verifyOptions.copy(publicKey = PublicKey(Base58.decode(s))))),
            opt[String]('s', "signature")
              .text("Signature to verify")
              .required()
              .action((s, c) => c.copy(verifyOptions = c.verifyOptions.copy(signature = ByteStr.decodeBase58(s).get))),
            opt[Boolean]("check-weak-pk")
              .abbr("cwpk")
              .text("Check for weak public key")
              .valueName("")
              .action((checkPk, c) => c.copy(verifyOptions = c.verifyOptions.copy(checkWeakPk = checkPk)))
          )
          .text("Sign bytes with provided private key")
          .action((_, c) => c.copy(mode = Command.SignBytes)),
        cmd("create-keys")
          .text("Generate key pair from seed")
          .action((_, c) => c.copy(mode = Command.CreateKeyPair))
          .children(
            opt[String]("seed-type")
              .validate {
                case "account" | "wallet" => success
                case _                    => failure("Invalid seed format")
              }
              .action((t, c) => c.copy(keyPairOptions = c.keyPairOptions.copy(seedType = t))),
            opt[Int]("nonce")
              .action((n, c) => c.copy(keyPairOptions = c.keyPairOptions.copy(nonce = n)))
          )
      ),
      cmd("transaction").children(
        cmd("serialize")
          .text("Serialize JSON transaction")
          .action((_, c) => c.copy(mode = Command.SerializeTx)),
        cmd("sign")
          .text("Sign JSON transaction")
          .action((_, c) => c.copy(mode = Command.SignTx))
          .children(
            opt[String]("signer-address")
              .abbr("sa")
              .text("Signer address (requires corresponding key in wallet.dat)")
              .action((a, c) => c.copy(signTxOptions = c.signTxOptions.copy(signerAddress = a)))
          ),
        cmd("sign-with-sk")
          .text("Sign JSON transaction with private key")
          .action((_, c) => c.copy(mode = Command.SignTxWithSk))
          .children(
            opt[String]("private-key")
              .abbr("sk")
              .text("Private key")
              .action((a, c) => c.copy(signOptions = c.signOptions.copy(privateKey = PrivateKey(Base58.decode(a)))))
          )
      ),
      help("help").hidden(),
      checkConfig(_.mode match {
        case null => failure("Command should be provided")
        case _    => success
      })
    )
  }

  // noinspection TypeAnnotation
  private[this] final class NodeState(c: Command) {
    lazy val settings = Application.loadApplicationConfig(c.configFile.map(new File(_)))
    lazy val wallet   = Wallet(settings.walletSettings)
    lazy val time     = new NTP(settings.ntpServer)
  }

  private[this] object Actions {
    type ActionResult = Either[String, Array[Byte]]

    @nowarn("cat=deprecation")
    def doCompile(settings: WavesSettings)(c: Command, str: Array[Byte]): ActionResult = {
      ScriptCompiler(new String(str), c.compileOptions.assetScript, settings.estimator)
        .map(_._1.bytes().arr)
    }

    def doDecompile(c: Command, data: Array[Byte]): ActionResult = {
      ScriptReader.fromBytes(data) match {
        case Left(value) =>
          Left(value.m)
        case Right(value) =>
          val (scriptText, _) = Script.decompile(value)
          Right(scriptText.getBytes(StandardCharsets.UTF_8))
      }
    }

    def doSign(c: Command, data: Array[Byte]): ActionResult =
      Right(com.wavesplatform.crypto.sign(c.signOptions.privateKey, data).arr)

    def doVerify(c: Command, data: Array[Byte]): ActionResult =
      Either.cond(
        com.wavesplatform.crypto.verify(c.verifyOptions.signature, data, c.verifyOptions.publicKey, c.verifyOptions.checkWeakPk),
        data,
        "Invalid signature"
      )

    def doCreateKeyPair(c: Command, data: Array[Byte]): ActionResult = {
      import com.wavesplatform.utils.byteStrFormat
      (c.keyPairOptions.seedType match {
        case "account" =>
          KeyPair.fromSeed(new String(data))
        case "wallet" =>
          Wallet(WalletSettings(None, Some("123"), Some(ByteStr(data))))
            .generateNewAccount(c.keyPairOptions.nonce)
            .toRight("Could not generate account")
      }).left
        .map(_.toString)
        .map(kp =>
          Json.toBytes(
            Json.obj(
              "publicKey"  -> kp.publicKey,
              "privateKey" -> kp.privateKey,
              "address"    -> kp.publicKey.toAddress,
              "walletSeed" -> ByteStr(data),
              "nonce"      -> c.keyPairOptions.nonce
            )
          )
        )
    }

    def doHash(c: Command, data: Array[Byte]): ActionResult = c.hashOptions.mode match {
      case "fast"   => Right(com.wavesplatform.crypto.fastHash(data))
      case "secure" => Right(com.wavesplatform.crypto.secureHash(data))
      case m        => Left(s"Invalid hashing mode: $m")
    }

    def doSerializeTx(c: Command, data: Array[Byte]): ActionResult = {
      val jsv = Json.parse(data)
      TransactionFactory
        .fromSignedRequest(jsv)
        .left
        .map(_.toString)
        .map(_.bytes())
    }

    def doSignTx(ns: NodeState)(c: Command, data: Array[Byte]): ActionResult =
      TransactionFactory
        .parseRequestAndSign(ns.wallet, c.signTxOptions.signerAddress, ns.time, Json.parse(data).as[JsObject])
        .left
        .map(_.toString)
        .map(tx => Json.toBytes(tx.json()))

    def doSignTxWithSK(c: Command, data: Array[Byte]): ActionResult = {
      import cats.syntax.either.*
      import com.wavesplatform.api.http.requests.InvokeScriptRequest.signedInvokeScriptRequestReads
      import com.wavesplatform.api.http.requests.SponsorFeeRequest.signedSponsorRequestFormat
      import com.wavesplatform.transaction.TransactionType.*

      val json = Json.parse(data)
      (TransactionType((json \ "type").as[Int]) match {
        case Issue           => json.as[IssueRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case Transfer        => json.as[TransferRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case Reissue         => json.as[ReissueRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case Burn            => json.as[BurnRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case Exchange        => json.as[ExchangeRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case Lease           => json.as[LeaseRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case LeaseCancel     => json.as[LeaseCancelRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case CreateAlias     => json.as[CreateAliasRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case MassTransfer    => json.as[SignedMassTransferRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case Data            => json.as[SignedDataRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case SetScript       => json.as[SignedSetScriptRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case SponsorFee      => json.as[SignedSponsorFeeRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case SetAssetScript  => json.as[SignedSetAssetScriptRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case InvokeScript    => json.as[SignedInvokeScriptRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case UpdateAssetInfo => json.as[SignedUpdateAssetInfoRequest].toTx.map(_.signWith(c.signOptions.privateKey))
        case other           => GenericError(s"Signing $other is not supported").asLeft[Transaction]
      }).leftMap(_.toString).map(_.json().toString().getBytes())
    }
  }

  private[this] object IO {
    def readInput(c: Command): Array[Byte] = {
      val inputStream = c.inputData match {
        case Input.StdIn =>
          System.in

        case Input.Str(s) =>
          new ByteArrayInputStream(s.utf8Bytes)

        case Input.File(file) =>
          new FileInputStream(file)
      }

      toPlainBytes(c.inFormat, ByteStreams.toByteArray(inputStream))
    }

    def writeOutput(c: Command, result: Array[Byte]): Unit = {
      val outputStream = c.outputFile match {
        case Some(file) => new FileOutputStream(file)
        case None       => System.out
      }

      val encodedBytes = encode(result, c.outFormat)
      outputStream.write(encodedBytes)
    }

    private[this] def encode(v: Array[Byte], format: String) = format match {
      case "plain"  => v
      case "base64" => Base64.encode(v).getBytes(StandardCharsets.US_ASCII)
      case "base58" => Base58.encode(v).getBytes(StandardCharsets.US_ASCII)
      case _        => sys.error(s"Invalid format $format")
    }

    private[this] def toPlainBytes(inFormat: String, encodedBytes: Array[Byte]) = {
      lazy val strWithoutSpaces = new String(encodedBytes).replaceAll("\\s+", "")
      inFormat match {
        case "plain"  => encodedBytes
        case "base58" => FastBase58.decode(strWithoutSpaces)
        case "base64" => Base64.decode(strWithoutSpaces)
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy