com.wavesplatform.api.http.DebugApiRoute.scala Maven / Gradle / Ivy
The newest version!
package com.wavesplatform.api.http
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.headers.Accept
import akka.http.scaladsl.server.Route
import com.typesafe.config.{ConfigObject, ConfigRenderOptions}
import com.wavesplatform.Version
import com.wavesplatform.account.{Address, PKKeyPair}
import com.wavesplatform.api.common.{CommonAccountsApi, CommonAssetsApi, CommonTransactionsApi, TransactionMeta}
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.database.RocksDBWriter
import com.wavesplatform.lang.ValidationError
import com.wavesplatform.mining.{Miner, MinerDebugInfo}
import com.wavesplatform.network.{PeerDatabase, PeerInfo, *}
import com.wavesplatform.settings.{RestAPISettings, WavesSettings}
import com.wavesplatform.state.diffs.TransactionDiffer
import com.wavesplatform.state.{Blockchain, Height, LeaseBalance, NG, Portfolio, SnapshotBlockchain, TxMeta}
import com.wavesplatform.transaction.*
import com.wavesplatform.transaction.Asset.IssuedAsset
import com.wavesplatform.transaction.TxValidationError.GenericError
import com.wavesplatform.transaction.smart.InvokeScriptTransaction
import com.wavesplatform.transaction.smart.script.trace.{InvokeScriptTrace, TracedResult}
import com.wavesplatform.utils.{ScorexLogging, Time, byteStrFormat}
import com.wavesplatform.utx.UtxPool
import com.wavesplatform.wallet.Wallet
import io.netty.channel.Channel
import monix.eval.{Coeval, Task}
import monix.execution.Scheduler
import play.api.libs.json.*
import play.api.libs.json.Json.JsValueWrapper
import java.net.{InetAddress, InetSocketAddress, URI}
import java.util.concurrent.ConcurrentMap
import scala.concurrent.Future
import scala.concurrent.duration.*
import scala.util.control.NonFatal
import scala.util.{Failure, Success}
case class DebugApiRoute(
ws: WavesSettings,
time: Time,
blockchain: Blockchain & NG,
wallet: Wallet,
accountsApi: CommonAccountsApi,
transactionsApi: CommonTransactionsApi,
assetsApi: CommonAssetsApi,
peerDatabase: PeerDatabase,
establishedConnections: ConcurrentMap[Channel, PeerInfo],
rollbackTask: (ByteStr, Boolean) => Task[Either[ValidationError, Unit]],
utxStorage: UtxPool,
miner: Miner & MinerDebugInfo,
historyReplier: HistoryReplierL1,
extLoaderStateReporter: Coeval[RxExtensionLoader.State],
mbsCacheSizesReporter: Coeval[MicroBlockSynchronizer.CacheSizes],
scoreReporter: Coeval[RxScoreObserver.Stats],
configRoot: ConfigObject,
db: RocksDBWriter,
priorityPoolBlockchain: () => Option[Blockchain],
routeTimeout: RouteTimeout,
heavyRequestScheduler: Scheduler
) extends ApiRoute
with AuthRoute
with ScorexLogging {
import DebugApiRoute.*
private lazy val configStr = configRoot.render(ConfigRenderOptions.concise().setJson(true).setFormatted(true))
private lazy val fullConfig: JsValue = Json.parse(configStr)
private lazy val wavesConfig: JsObject = Json.obj("waves" -> (fullConfig \ "waves").get)
override val settings: RestAPISettings = ws.restAPISettings
private[this] val serializer = TransactionJsonSerializer(blockchain)
override lazy val route: Route = pathPrefix("debug") {
balanceHistory ~ stateHash ~ validate ~ withAuth {
state ~ info ~ stateWaves ~ rollback ~ blacklist ~ minerInfo ~ configInfo ~ print
}
}
def print: Route =
path("print")(jsonPost[DebugMessage] { params =>
log.debug(params.message.take(250))
""
})
def balanceHistory: Route = (path("balances" / "history" / AddrSegment) & get) { address =>
complete(Json.toJson(db.loadBalanceHistory(address).map { case (h, b) =>
Json.obj("height" -> h, "balance" -> b)
}))
}
private def distribution(height: Int): Route = optionalHeaderValueByType(Accept) { accept =>
routeTimeout.executeToFuture {
assetsApi
.wavesDistribution(height, None)
.toListL
.map {
case l if accept.exists(_.mediaRanges.exists(CustomJson.acceptsNumbersAsStrings)) =>
Json.obj(l.map { case (address, balance) => address.toString -> (balance.toString: JsValueWrapper) }*)
case l =>
Json.obj(l.map { case (address, balance) => address.toString -> (balance: JsValueWrapper) }*)
}
}
}
def state: Route = (path("state") & get) {
distribution(blockchain.height)
}
def stateWaves: Route = (path("stateWaves" / IntNumber) & get) { height =>
distribution(height)
}
private def rollbackToBlock(blockId: ByteStr, returnTransactionsToUtx: Boolean): Future[Either[ValidationError, JsObject]] = {
implicit val sc: Scheduler = heavyRequestScheduler
rollbackTask(blockId, returnTransactionsToUtx)
.map(_.map(_ => Json.obj("BlockId" -> blockId.toString)))
.runAsyncLogErr
}
def rollback: Route = (path("rollback") & withRequestTimeout(15.minutes)) {
jsonPost[RollbackParams] { params =>
blockchain.blockHeader(params.rollbackTo) match {
case Some(sh) =>
rollbackToBlock(sh.id(), params.returnTransactionsToUtx)
case None =>
(StatusCodes.BadRequest, "Block at height not found")
}
} ~ complete(StatusCodes.BadRequest)
}
def info: Route = (path("info") & get) {
complete(
Json.obj(
"stateHeight" -> blockchain.height,
"extensionLoaderState" -> extLoaderStateReporter().toString,
"historyReplierCacheSizes" -> Json.toJson(historyReplier.cacheSizes),
"microBlockSynchronizerCacheSizes" -> Json.toJson(mbsCacheSizesReporter()),
"scoreObserverStats" -> Json.toJson(scoreReporter()),
"minerState" -> Json.toJson(miner.state)
)
)
}
def minerInfo: Route = (path("minerInfo") & get) {
complete {
val accounts = if (ws.minerSettings.privateKeys.nonEmpty) {
ws.minerSettings.privateKeys.map(PKKeyPair(_))
} else {
wallet.privateKeyAccounts
}
accounts
.filterNot(account => blockchain.hasAccountScript(account.toAddress))
.map { account =>
(account.toAddress, miner.getNextBlockGenerationOffset(account))
}
.collect { case (address, Right(offset)) =>
AccountMiningInfo(
address.toString,
blockchain.effectiveBalance(
address,
ws.blockchainSettings.functionalitySettings.generatingBalanceDepth(blockchain.height),
blockchain.microblockIds.lastOption
),
System.currentTimeMillis() + offset.toMillis
)
}
}
}
def configInfo: Route = (path("configInfo") & get & parameter("full".as[Boolean])) { full =>
complete(if (full) fullConfig else wavesConfig)
}
def blacklist: Route = (path("blacklist") & post) {
entity(as[String]) { socketAddressString =>
try {
val uri = new URI("node://" + socketAddressString)
val address = InetAddress.getByName(uri.getHost)
establishedConnections.entrySet().stream().forEach { entry =>
entry.getValue.remoteAddress match {
case x: InetSocketAddress if x.getAddress == address =>
peerDatabase.blacklistAndClose(entry.getKey, "Debug API request")
case _ =>
}
}
peerDatabase.blacklist(address, "Debug API request")
complete(StatusCodes.OK)
} catch {
case NonFatal(_) => complete(StatusCodes.BadRequest)
}
} ~ complete(StatusCodes.BadRequest)
}
def validate: Route =
path("validate")(jsonPost[JsObject] { jsv =>
val resBlockchain = priorityPoolBlockchain().getOrElse(blockchain)
val startTime = System.nanoTime()
val parsedTransaction = TransactionFactory.fromSignedRequest(jsv)
val tracedSnapshot = for {
tx <- TracedResult(parsedTransaction)
diff <- TransactionDiffer.forceValidate(resBlockchain.lastBlockTimestamp, time.correctedTime(), enableExecutionLog = true)(resBlockchain, tx)
} yield (tx, diff)
val error = tracedSnapshot.resultE match {
case Right((tx, diff)) => diff.errorMessage(tx.id()).map(em => GenericError(em.text))
case Left(err) => Some(err)
}
val transactionJson = parsedTransaction.fold(_ => jsv, _.json())
val serializer = tracedSnapshot.resultE
.fold(
_ => this.serializer,
{ case (_, snapshot) =>
val snapshotBlockchain = SnapshotBlockchain(resBlockchain, snapshot)
this.serializer.copy(blockchain = snapshotBlockchain)
}
)
val extendedJson = tracedSnapshot.resultE
.fold(
_ => jsv,
{ case (tx, diff) =>
val meta = tx match {
case ist: InvokeScriptTransaction =>
val result = diff.scriptResults.get(ist.id())
TransactionMeta.Invoke(Height(resBlockchain.height), ist, TxMeta.Status.Succeeded, diff.scriptsComplexity, result)
case tx => TransactionMeta.Default(Height(resBlockchain.height), tx, TxMeta.Status.Succeeded, diff.scriptsComplexity)
}
serializer.transactionWithMetaJson(meta)
}
)
val response = Json.obj(
"valid" -> error.isEmpty,
"validationTime" -> (System.nanoTime() - startTime).nanos.toMillis,
"trace" -> tracedSnapshot.trace.map {
case ist: InvokeScriptTrace => ist.maybeLoggedJson(logged = true)(serializer.invokeScriptResultWrites)
case trace => trace.loggedJson
},
"height" -> resBlockchain.height
)
error.fold(response ++ extendedJson)(err =>
response + ("error" -> JsString(ApiError.fromValidationError(err).message)) + ("transaction" -> transactionJson)
)
})
def stateHash: Route = (get & pathPrefix("stateHash")) {
path("last")(stateHashAt(blockchain.height - 1)) ~ path(IntNumber)(stateHashAt)
}
private def stateHashAt(height: Int): Route = {
val result = for {
sh <- db.loadStateHash(height)
h <- blockchain.blockHeader(height)
} yield Json.toJson(sh).as[JsObject] ++ Json.obj(
"snapshotHash" -> db.snapshotStateHash(height),
"blockId" -> h.id().toString,
"baseTarget" -> h.header.baseTarget,
"height" -> height,
"version" -> Version.VersionString
)
result match {
case Some(value) => complete(value)
case None => complete(StatusCodes.NotFound)
}
}
}
object DebugApiRoute {
implicit val assetsFormat: Format[Map[ByteStr, Long]] = Format[Map[ByteStr, Long]](
{
case JsObject(m) =>
m.foldLeft[JsResult[Map[ByteStr, Long]]](JsSuccess(Map.empty)) {
case (e: JsError, _) => e
case (JsSuccess(m, _), (rawAssetId, JsNumber(count))) =>
(ByteStr.decodeBase58(rawAssetId), count) match {
case (Success(assetId), count) if count.isValidLong => JsSuccess(m.updated(assetId, count.toLong))
case (Failure(_), _) => JsError(s"Can't parse '$rawAssetId' as base58 string")
case (_, count) => JsError(s"Invalid count of assets: $count")
}
case (_, (_, rawCount)) =>
JsError(s"Invalid count of assets: $rawCount")
}
case _ => JsError("The map is expected")
},
m => Json.toJson(m.map { case (assetId, count) => assetId.toString -> count })
)
implicit val leaseInfoFormat: Format[LeaseBalance] = Json.format
case class AccountMiningInfo(address: String, miningBalance: Long, timestamp: Long)
implicit val accountMiningBalanceFormat: Format[AccountMiningInfo] = Json.format
implicit val addressWrites: Writes[Address] = Writes((a: Address) => JsString(a.toString))
implicit val hrCacheSizesFormat: Format[HistoryReplierL1.CacheSizes] = Json.format
implicit val mbsCacheSizesFormat: Format[MicroBlockSynchronizer.CacheSizes] = Json.format
implicit val BigIntWrite: Writes[BigInt] = (bigInt: BigInt) => JsNumber(BigDecimal(bigInt))
implicit val scoreReporterStatsWrite: Writes[RxScoreObserver.Stats] = Json.writes[RxScoreObserver.Stats]
import MinerDebugInfo.*
implicit val minerStateWrites: Writes[MinerDebugInfo.State] = (s: MinerDebugInfo.State) =>
JsString(s match {
case MiningBlocks => "mining blocks"
case MiningMicroblocks => "mining microblocks"
case Disabled => "disabled"
case Error(err) => s"error: $err"
})
implicit val assetMapWrites: Writes[Map[IssuedAsset, Long]] = Writes { m =>
Json.toJson(m.map { case (asset, balance) =>
asset.id.toString -> JsNumber(balance)
})
}
implicit val portfolioJsonWrites: Writes[Portfolio] = Writes { pf =>
JsObject(
Map(
"balance" -> JsNumber(pf.balance),
"lease" -> Json.toJson(pf.lease),
"assets" -> Json.toJson(pf.assets)
)
)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy