|
|
|
package com.xsn.explorer.services
|
|
|
|
|
|
|
|
import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureOps, OrOps}
|
|
|
|
import com.alexitc.playsonify.core.{ApplicationResult, FutureApplicationResult}
|
|
|
|
import com.alexitc.playsonify.models.ApplicationError
|
|
|
|
import com.xsn.explorer.config.{ExplorerConfig, RPCConfig}
|
|
|
|
import com.xsn.explorer.errors._
|
|
|
|
import com.xsn.explorer.executors.ExternalServiceExecutionContext
|
|
|
|
import com.xsn.explorer.models._
|
|
|
|
import com.xsn.explorer.models.values._
|
|
|
|
import javax.inject.Inject
|
|
|
|
import org.scalactic.{Bad, Good}
|
|
|
|
import org.slf4j.LoggerFactory
|
|
|
|
import play.api.libs.json.{JsNull, JsValue, Reads}
|
|
|
|
import play.api.libs.ws.{WSAuthScheme, WSClient, WSResponse}
|
|
|
|
|
|
|
|
import scala.util.Try
|
|
|
|
|
|
|
|
trait XSNService {
|
|
|
|
|
|
|
|
def getTransaction(txid: TransactionId): FutureApplicationResult[rpc.Transaction[rpc.TransactionVIN]]
|
|
|
|
|
|
|
|
def getRawTransaction(txid: TransactionId): FutureApplicationResult[JsValue]
|
|
|
|
|
|
|
|
def getAddressBalance(address: Address): FutureApplicationResult[rpc.AddressBalance]
|
|
|
|
|
|
|
|
def getTransactions(address: Address): FutureApplicationResult[List[TransactionId]]
|
|
|
|
|
|
|
|
def getBlock(blockhash: Blockhash): FutureApplicationResult[rpc.Block]
|
|
|
|
|
|
|
|
def getRawBlock(blockhash: Blockhash): FutureApplicationResult[JsValue]
|
|
|
|
|
|
|
|
def getBlockhash(height: Height): FutureApplicationResult[Blockhash]
|
|
|
|
|
|
|
|
def getLatestBlock(): FutureApplicationResult[rpc.Block]
|
|
|
|
|
|
|
|
def getServerStatistics(): FutureApplicationResult[rpc.ServerStatistics]
|
|
|
|
|
|
|
|
def getMasternodeCount(): FutureApplicationResult[Int]
|
|
|
|
|
|
|
|
def getDifficulty(): FutureApplicationResult[BigDecimal]
|
|
|
|
|
|
|
|
def getMasternodes(): FutureApplicationResult[List[rpc.Masternode]]
|
|
|
|
|
|
|
|
def getMasternode(ipAddress: IPAddress): FutureApplicationResult[rpc.Masternode]
|
|
|
|
|
|
|
|
def getUnspentOutputs(address: Address): FutureApplicationResult[JsValue]
|
|
|
|
|
|
|
|
def sendRawTransaction(hex: HexString): FutureApplicationResult[String]
|
|
|
|
|
|
|
|
def cleanGenesisBlock(block: rpc.Block): rpc.Block = {
|
|
|
|
Option(block)
|
|
|
|
.filter(_.hash == genesisBlockhash)
|
|
|
|
.map(_.copy(transactions = List.empty))
|
|
|
|
.getOrElse(block)
|
|
|
|
}
|
|
|
|
|
|
|
|
def genesisBlockhash: Blockhash
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
class XSNServiceRPCImpl @Inject() (
|
|
|
|
ws: WSClient,
|
|
|
|
rpcConfig: RPCConfig,
|
|
|
|
explorerConfig: ExplorerConfig)(
|
|
|
|
implicit ec: ExternalServiceExecutionContext)
|
|
|
|
extends XSNService {
|
|
|
|
|
|
|
|
private val logger = LoggerFactory.getLogger(this.getClass)
|
|
|
|
|
|
|
|
private val server = ws.url(rpcConfig.host.string)
|
|
|
|
.withAuth(rpcConfig.username.string, rpcConfig.password.string, WSAuthScheme.BASIC)
|
|
|
|
.withHttpHeaders("Content-Type" -> "text/plain")
|
|
|
|
|
|
|
|
|
|
|
|
override def getTransaction(txid: TransactionId): FutureApplicationResult[rpc.Transaction[rpc.TransactionVIN]] = {
|
|
|
|
val errorCodeMapper = Map(-5 -> TransactionNotFoundError)
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(s"""{ "jsonrpc": "1.0", "method": "getrawtransaction", "params": ["${txid.string}", 1] }""")
|
|
|
|
.map { response =>
|
|
|
|
|
|
|
|
val maybe = getResult[rpc.Transaction[rpc.TransactionVIN]](response, errorCodeMapper)
|
|
|
|
maybe.getOrElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, txid = ${txid.string}, status = ${response.status}, response = ${response.body}")
|
|
|
|
|
|
|
|
Bad(XSNUnexpectedResponseError).accumulating
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override def getRawTransaction(txid: TransactionId): FutureApplicationResult[JsValue] = {
|
|
|
|
val errorCodeMapper = Map(-5 -> TransactionNotFoundError)
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(s"""{ "jsonrpc": "1.0", "method": "getrawtransaction", "params": ["${txid.string}", 1] }""")
|
|
|
|
.map { response =>
|
|
|
|
|
|
|
|
val maybe = getResult[JsValue](response, errorCodeMapper)
|
|
|
|
maybe.getOrElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, txid = ${txid.string}, status = ${response.status}, response = ${response.body}")
|
|
|
|
|
|
|
|
Bad(XSNUnexpectedResponseError).accumulating
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override def getAddressBalance(address: Address): FutureApplicationResult[rpc.AddressBalance] = {
|
|
|
|
val body = s"""
|
|
|
|
|{
|
|
|
|
| "jsonrpc": "1.0",
|
|
|
|
| "method": "getaddressbalance",
|
|
|
|
| "params": [
|
|
|
|
| { "addresses": ["${address.string}"] }
|
|
|
|
| ]
|
|
|
|
|}
|
|
|
|
|""".stripMargin
|
|
|
|
|
|
|
|
// the network returns 0 for valid addresses
|
|
|
|
val errorCodeMapper = Map(-5 -> AddressFormatError)
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(body)
|
|
|
|
.map { response =>
|
|
|
|
|
|
|
|
val maybe = getResult[rpc.AddressBalance](response, errorCodeMapper)
|
|
|
|
maybe.getOrElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, address = ${address.string}, response = ${response.body}")
|
|
|
|
|
|
|
|
Bad(XSNUnexpectedResponseError).accumulating
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override def getTransactions(address: Address): FutureApplicationResult[List[TransactionId]] = {
|
|
|
|
val body = s"""
|
|
|
|
|{
|
|
|
|
| "jsonrpc": "1.0",
|
|
|
|
| "method": "getaddresstxids",
|
|
|
|
| "params": [
|
|
|
|
| { "addresses": ["${address.string}"] }
|
|
|
|
| ]
|
|
|
|
|}
|
|
|
|
|""".stripMargin
|
|
|
|
|
|
|
|
// the network returns 0 for valid addresses
|
|
|
|
val errorCodeMapper = Map(-5 -> AddressFormatError)
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(body)
|
|
|
|
.map { response =>
|
|
|
|
|
|
|
|
val maybe = getResult[List[TransactionId]](response, errorCodeMapper)
|
|
|
|
maybe.getOrElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, address = ${address.string}, response = ${response.body}")
|
|
|
|
|
|
|
|
Bad(XSNUnexpectedResponseError).accumulating
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override def getBlock(blockhash: Blockhash): FutureApplicationResult[rpc.Block] = {
|
|
|
|
val errorCodeMapper = Map(-5 -> BlockNotFoundError)
|
|
|
|
val body = s"""{ "jsonrpc": "1.0", "method": "getblock", "params": ["${blockhash.string}"] }"""
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(body)
|
|
|
|
.map { response =>
|
|
|
|
|
|
|
|
val maybe = getResult[rpc.Block](response, errorCodeMapper)
|
|
|
|
maybe
|
|
|
|
.map {
|
|
|
|
case Good(block) => Good(cleanGenesisBlock(block))
|
|
|
|
case x => x
|
|
|
|
}
|
|
|
|
.getOrElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, blockhash = ${blockhash.string}, status = ${response.status}, response = ${response.body}")
|
|
|
|
|
|
|
|
Bad(XSNUnexpectedResponseError).accumulating
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override def getRawBlock(blockhash: Blockhash): FutureApplicationResult[JsValue] = {
|
|
|
|
val errorCodeMapper = Map(-5 -> BlockNotFoundError)
|
|
|
|
val body = s"""{ "jsonrpc": "1.0", "method": "getblock", "params": ["${blockhash.string}"] }"""
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(body)
|
|
|
|
.map { response =>
|
|
|
|
|
|
|
|
val maybe = getResult[JsValue](response, errorCodeMapper)
|
|
|
|
maybe.getOrElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, blockhash = ${blockhash.string}, status = ${response.status}, response = ${response.body}")
|
|
|
|
|
|
|
|
Bad(XSNUnexpectedResponseError).accumulating
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override def getBlockhash(height: Height): FutureApplicationResult[Blockhash] = {
|
|
|
|
val errorCodeMapper = Map(-8 -> BlockNotFoundError)
|
|
|
|
val body = s"""{ "jsonrpc": "1.0", "method": "getblockhash", "params": [${height.int}] }"""
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(body)
|
|
|
|
.map { response =>
|
|
|
|
|
|
|
|
val maybe = getResult[Blockhash](response, errorCodeMapper)
|
|
|
|
maybe.getOrElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, blockhash = ${height.int}, status = ${response.status}, response = ${response.body}")
|
|
|
|
|
|
|
|
Bad(XSNUnexpectedResponseError).accumulating
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override def getLatestBlock(): FutureApplicationResult[rpc.Block] = {
|
|
|
|
val body = s"""
|
|
|
|
|{
|
|
|
|
| "jsonrpc": "1.0",
|
|
|
|
| "method": "getbestblockhash",
|
|
|
|
| "params": []
|
|
|
|
|}
|
|
|
|
|""".stripMargin
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(body)
|
|
|
|
.flatMap { response =>
|
|
|
|
|
|
|
|
val result = for {
|
|
|
|
blockhash <- getResult[Blockhash](response)
|
|
|
|
.orElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, response = ${response.body}")
|
|
|
|
None
|
|
|
|
}
|
|
|
|
.getOrElse(Bad(XSNUnexpectedResponseError).accumulating)
|
|
|
|
.toFutureOr
|
|
|
|
|
|
|
|
block <- getBlock(blockhash).
|
|
|
|
map {
|
|
|
|
case Good(block) => Good(cleanGenesisBlock(block))
|
|
|
|
case x => x
|
|
|
|
}
|
|
|
|
.toFutureOr
|
|
|
|
} yield block
|
|
|
|
|
|
|
|
result.toFuture
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override def getServerStatistics(): FutureApplicationResult[rpc.ServerStatistics] = {
|
|
|
|
val body = s"""
|
|
|
|
|{
|
|
|
|
| "jsonrpc": "1.0",
|
|
|
|
| "method": "gettxoutsetinfo",
|
|
|
|
| "params": []
|
|
|
|
|}
|
|
|
|
|""".stripMargin
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(body)
|
|
|
|
.map { response =>
|
|
|
|
val maybe = getResult[rpc.ServerStatistics](response)
|
|
|
|
maybe.getOrElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, response = ${response.body}")
|
|
|
|
|
|
|
|
Bad(XSNUnexpectedResponseError).accumulating
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override def getMasternodeCount(): FutureApplicationResult[Int] = {
|
|
|
|
val body = s"""
|
|
|
|
|{
|
|
|
|
| "jsonrpc": "1.0",
|
|
|
|
| "method": "masternode",
|
|
|
|
| "params": ["count"]
|
|
|
|
|}
|
|
|
|
|""".stripMargin
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(body)
|
|
|
|
.map { response =>
|
|
|
|
val maybe = getResult[Int](response)
|
|
|
|
maybe.getOrElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, response = ${response.body}")
|
|
|
|
|
|
|
|
Bad(XSNUnexpectedResponseError).accumulating
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override def getDifficulty(): FutureApplicationResult[BigDecimal] = {
|
|
|
|
val body = s"""
|
|
|
|
|{
|
|
|
|
| "jsonrpc": "1.0",
|
|
|
|
| "method": "getdifficulty",
|
|
|
|
| "params": []
|
|
|
|
|}
|
|
|
|
|""".stripMargin
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(body)
|
|
|
|
.map { response =>
|
|
|
|
val maybe = getResult[BigDecimal](response)
|
|
|
|
maybe.getOrElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, response = ${response.body}")
|
|
|
|
|
|
|
|
Bad(XSNUnexpectedResponseError).accumulating
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override def getMasternodes(): FutureApplicationResult[List[rpc.Masternode]] = {
|
|
|
|
val body = s"""
|
|
|
|
|{
|
|
|
|
| "jsonrpc": "1.0",
|
|
|
|
| "method": "masternode",
|
|
|
|
| "params": ["list", "full"]
|
|
|
|
|}
|
|
|
|
|""".stripMargin
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(body)
|
|
|
|
.map { response =>
|
|
|
|
val maybe = getResult[Map[String, String]](response)
|
|
|
|
.map {
|
|
|
|
case Good(map) => Good(rpc.Masternode.fromMap(map))
|
|
|
|
case Bad(errors) => Bad(errors)
|
|
|
|
}
|
|
|
|
|
|
|
|
maybe.getOrElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, response = ${response.body}")
|
|
|
|
|
|
|
|
Bad(XSNUnexpectedResponseError).accumulating
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override def getMasternode(ipAddress: IPAddress): FutureApplicationResult[rpc.Masternode] = {
|
|
|
|
val body = s"""
|
|
|
|
|{
|
|
|
|
| "jsonrpc": "1.0",
|
|
|
|
| "method": "masternode",
|
|
|
|
| "params": ["list", "full", "${ipAddress.string}"]
|
|
|
|
|}
|
|
|
|
|""".stripMargin
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(body)
|
|
|
|
.map { response =>
|
|
|
|
val maybe = getResult[Map[String, String]](response)
|
|
|
|
.map {
|
|
|
|
case Good(map) =>
|
|
|
|
rpc.Masternode
|
|
|
|
.fromMap(map)
|
|
|
|
.headOption
|
|
|
|
.map(Good(_))
|
|
|
|
.getOrElse(Bad(MasternodeNotFoundError).accumulating)
|
|
|
|
|
|
|
|
case Bad(errors) => Bad(errors)
|
|
|
|
}
|
|
|
|
|
|
|
|
maybe.getOrElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, response = ${response.body}")
|
|
|
|
|
|
|
|
Bad(XSNUnexpectedResponseError).accumulating
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override def getUnspentOutputs(address: Address): FutureApplicationResult[JsValue] = {
|
|
|
|
val body = s"""
|
|
|
|
|{
|
|
|
|
| "jsonrpc": "1.0",
|
|
|
|
| "method": "getaddressutxos",
|
|
|
|
| "params": [
|
|
|
|
| { "addresses": ["${address.string}"] }
|
|
|
|
| ]
|
|
|
|
|}
|
|
|
|
|""".stripMargin
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(body)
|
|
|
|
.map { response =>
|
|
|
|
val maybe = getResult[JsValue](response)
|
|
|
|
maybe.getOrElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, response = ${response.body}")
|
|
|
|
|
|
|
|
Bad(XSNUnexpectedResponseError).accumulating
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override def sendRawTransaction(hex: HexString): FutureApplicationResult[String] = {
|
|
|
|
val errorCodeMapper = Map(
|
|
|
|
-26 -> InvalidRawTransactionError,
|
|
|
|
-22 -> InvalidRawTransactionError,
|
|
|
|
-27 -> RawTransactionAlreadyExistsError)
|
|
|
|
|
|
|
|
val body = s"""
|
|
|
|
|{
|
|
|
|
| "jsonrpc": "1.0",
|
|
|
|
| "method": "sendrawtransaction",
|
|
|
|
| "params": ["${hex.string}"]
|
|
|
|
|}
|
|
|
|
|""".stripMargin
|
|
|
|
|
|
|
|
server
|
|
|
|
.post(body)
|
|
|
|
.map { response =>
|
|
|
|
val maybe = getResult[String](response, errorCodeMapper).map { _.map(_.toString()) }
|
|
|
|
|
|
|
|
maybe.getOrElse {
|
|
|
|
logger.warn(s"Unexpected response from XSN Server, status = ${response.status}, response = ${response.body}")
|
|
|
|
|
|
|
|
Bad(XSNUnexpectedResponseError).accumulating
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private def mapError(json: JsValue, errorCodeMapper: Map[Int, ApplicationError]): Option[ApplicationError] = {
|
|
|
|
val jsonErrorMaybe = (json \ "error")
|
|
|
|
.asOpt[JsValue]
|
|
|
|
.filter(_ != JsNull)
|
|
|
|
|
|
|
|
val errorMaybe = jsonErrorMaybe
|
|
|
|
.flatMap { jsonError =>
|
|
|
|
// from error code if possible
|
|
|
|
(jsonError \ "code")
|
|
|
|
.asOpt[Int]
|
|
|
|
.flatMap(errorCodeMapper.get)
|
|
|
|
.orElse {
|
|
|
|
// from message
|
|
|
|
(jsonError \ "message")
|
|
|
|
.asOpt[String]
|
|
|
|
.filter(_.nonEmpty)
|
|
|
|
.map(XSNMessageError.apply)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
errorMaybe
|
|
|
|
.collect {
|
|
|
|
case XSNMessageError("Work queue depth exceeded") => XSNWorkQueueDepthExceeded
|
|
|
|
}
|
|
|
|
.orElse(errorMaybe)
|
|
|
|
}
|
|
|
|
|
|
|
|
private def getResult[A](
|
|
|
|
response: WSResponse,
|
|
|
|
errorCodeMapper: Map[Int, ApplicationError] = Map.empty)(
|
|
|
|
implicit reads: Reads[A]): Option[ApplicationResult[A]] = {
|
|
|
|
|
|
|
|
val maybe = Option(response)
|
|
|
|
.filter(_.status == 200)
|
|
|
|
.flatMap { r => Try(r.json).toOption }
|
|
|
|
.flatMap { json =>
|
|
|
|
if (logger.isDebugEnabled) {
|
|
|
|
val x = (json \ "result").validate[A]
|
|
|
|
x.asEither.left.foreach { errors =>
|
|
|
|
val msg = errors
|
|
|
|
.map { case (path, error) => path.toJsonString -> error.toString() }
|
|
|
|
.mkString(", ")
|
|
|
|
logger.debug(s"Failed to decode result, errors = ${msg}")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
(json \ "result")
|
|
|
|
.asOpt[A]
|
|
|
|
.map { Good(_) }
|
|
|
|
.orElse {
|
|
|
|
mapError(json, errorCodeMapper)
|
|
|
|
.map(Bad.apply)
|
|
|
|
.map(_.accumulating)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
maybe
|
|
|
|
.orElse {
|
|
|
|
// if there is no result nor error, it is probably that the server returned non 200 status
|
|
|
|
Try(response.json)
|
|
|
|
.toOption
|
|
|
|
.flatMap { json => mapError(json, errorCodeMapper) }
|
|
|
|
.map { e => Bad(e).accumulating }
|
|
|
|
}
|
|
|
|
.orElse {
|
|
|
|
// if still there is no error, the response might not be a json
|
|
|
|
Try(response.body)
|
|
|
|
.collect {
|
|
|
|
case "Work queue depth exceeded" => Bad(XSNWorkQueueDepthExceeded).accumulating
|
|
|
|
}
|
|
|
|
.toOption
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override val genesisBlockhash: Blockhash = explorerConfig.genesisBlock
|
|
|
|
|
|
|
|
}
|