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

com.wavesplatform.api.http.BlocksApiRoute.scala Maven / Gradle / Ivy

The newest version!
package com.wavesplatform.api.http

import scala.annotation.tailrec
import scala.util.Try
import akka.http.scaladsl.server.{Route, StandardRoute}
import cats.syntax.either.*
import com.wavesplatform.api.BlockMeta
import com.wavesplatform.api.common.CommonBlocksApi
import com.wavesplatform.api.http.ApiError.{BlockDoesNotExist, TooBigArrayAllocation}
import com.wavesplatform.block.Block
import com.wavesplatform.settings.RestAPISettings
import com.wavesplatform.state.TxMeta
import com.wavesplatform.transaction.Asset.Waves
import com.wavesplatform.transaction.Transaction
import com.wavesplatform.transaction.TxValidationError.GenericError
import com.wavesplatform.utils.Time
import play.api.libs.json.*

case class BlocksApiRoute(settings: RestAPISettings, commonApi: CommonBlocksApi, time: Time, routeTimeout: RouteTimeout) extends ApiRoute {
  import BlocksApiRoute.*

  override lazy val route: Route = (pathPrefix("blocks") & get) {
    path("at" / IntNumber) { height =>
      at(height, includeTransactions = true)
    } ~ path("seq" / IntNumber / IntNumber) { (start, end) =>
      seq(start, end, includeTransactions = true)
    } ~ path("last") {
      at(commonApi.currentHeight, includeTransactions = true)
    } ~ path("height") {
      complete(Json.obj("height" -> commonApi.currentHeight))
    } ~ path("delay" / BlockId / IntNumber) { (blockId, count) =>
      if (count > MaxBlocksForDelay) {
        complete(TooBigArrayAllocation(MaxBlocksForDelay))
      } else {
        complete(
          commonApi
            .blockDelay(blockId, count)
            .map(delay => Json.obj("delay" -> delay))
            .toRight(BlockDoesNotExist)
        )
      }
    } ~ path("height" / BlockId) { signature =>
      complete(for {
        meta <- commonApi.meta(signature).toRight(BlockDoesNotExist)
      } yield Json.obj("height" -> meta.height))
    } ~ path("address" / AddrSegment / IntNumber / IntNumber) { (address, start, end) =>
      if (end >= 0 && start >= 0 && end - start >= 0 && end - start < settings.blocksRequestLimit) {
        routeTimeout.executeToFuture {
          commonApi
            .blocksRange(start, end, address)
            .map(toJson)
            .toListL
        }
      } else {
        complete(TooBigArrayAllocation)
      }
    } ~ pathPrefix("headers") {
      path("at" / IntNumber) { height =>
        at(height, includeTransactions = false)
      } ~ path("seq" / IntNumber / IntNumber) { (start, end) =>
        seq(start, end, includeTransactions = false)
      } ~ path("last") {
        at(commonApi.currentHeight, includeTransactions = false)
      } ~ path(BlockId) { id =>
        complete(commonApi.meta(id).map(_.json()).toRight(BlockDoesNotExist))
      }
    } ~ path("heightByTimestamp" / LongNumber) { timestamp =>
      val heightE = for {
        _ <- Either.cond(timestamp <= time.correctedTime(), (), "Indicated timestamp belongs to the future")
        genesisTimestamp = commonApi.metaAtHeight(1).fold(0L)(_.header.timestamp)
        _      <- Either.cond(timestamp >= genesisTimestamp, (), "Indicated timestamp is before the start of the blockchain")
        result <- Try(heightByTimestamp(timestamp)).toEither.leftMap(_.getMessage)
      } yield result

      complete(heightE.bimap(GenericError(_), h => Json.obj("height" -> h)))
    } ~ path(BlockId) { id =>
      complete(commonApi.block(id).map(toJson).toRight(BlockDoesNotExist))
    }
  }

  private def at(height: Int, includeTransactions: Boolean): StandardRoute = {
    val result =
      if (includeTransactions)
        commonApi.blockAtHeight(height).map(toJson)
      else
        commonApi.metaAtHeight(height).map(_.json())

    complete(result.toRight(BlockDoesNotExist))
  }

  private def seq(start: Int, end: Int, includeTransactions: Boolean): Route = {
    if (end >= 0 && start >= 0 && end - start >= 0 && end - start < settings.blocksRequestLimit) {
      routeTimeout.executeToFuture {
        val blocks = if (includeTransactions) {
          commonApi
            .blocksRange(start, end)
            .map(toJson)
        } else {
          commonApi
            .metaRange(start, end)
            .map(_.json())
        }
        blocks.toListL.map(JsArray(_))
      }
    } else {
      complete(TooBigArrayAllocation)
    }
  }

  @throws[IllegalStateException]("if the state is altered while executing")
  private[this] def heightByTimestamp(target: Long): Int = {
    def timestampOf(height: Int, default: => Long = throw new IllegalStateException("State was altered")): Long =
      commonApi.metaAtHeight(height).fold(default)(_.header.timestamp)

    @tailrec
    def findHeightRec(lowerBound: Int = 1, upperBound: Int = commonApi.currentHeight): Int = {
      val lowerTimestamp = timestampOf(lowerBound)
      val upperTimestamp = timestampOf(upperBound)

      require(lowerTimestamp <= target)

      val averageBlockTime = (upperTimestamp - lowerTimestamp) / (upperBound - lowerBound).max(1)
      val offset = {
        val blocksBetween = ((target - lowerTimestamp) / averageBlockTime).toInt
        blocksBetween
      }

      val predictedHeight = (lowerBound + offset).max(lowerBound).min(upperBound)

      val timestamp      = timestampOf(predictedHeight)
      val rightTimestamp = timestampOf(predictedHeight + 1, Long.MaxValue)
      val leftHit        = timestamp <= target
      val rightHit       = rightTimestamp <= target

      val (newLower, newUpper) = {
        if (!leftHit) (lowerBound, (predictedHeight - 1).max(lowerBound))
        else if (rightHit) ((predictedHeight + 1).min(upperBound), upperBound)
        else (predictedHeight, predictedHeight)
      }

      if (newLower == newUpper) predictedHeight else findHeightRec(newLower, newUpper)
    }

    findHeightRec()
  }
}

object BlocksApiRoute {
  val MaxBlocksForDelay = 10000

  private def toJson(v: (BlockMeta, Seq[(TxMeta, Transaction)])): JsObject = v match {
    case (meta, transactions) =>
      meta.json() ++ transactionField(meta.header.version, transactions)
  }

  private def transactionField(blockVersion: Byte, transactions: Seq[(TxMeta, Transaction)]): JsObject = Json.obj(
    "fee" -> transactions.map(_._2.assetFee).collect { case (Waves, feeAmt) => feeAmt }.sum,
    "transactions" -> JsArray(transactions.map { case (tm, transaction) =>
      transaction.json() ++ TransactionJsonSerializer.applicationStatus(blockVersion >= Block.ProtoBlockVersion, tm.status)
    })
  )
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy