diff --git a/server/app/com/xsn/explorer/services/LedgerSynchronizerService.scala b/server/app/com/xsn/explorer/services/LedgerSynchronizerService.scala index c9b6cbc..d0eba3b 100644 --- a/server/app/com/xsn/explorer/services/LedgerSynchronizerService.scala +++ b/server/app/com/xsn/explorer/services/LedgerSynchronizerService.scala @@ -16,6 +16,7 @@ import scala.concurrent.{ExecutionContext, Future} class LedgerSynchronizerService @Inject() ( xsnService: XSNService, transactionService: TransactionService, + transactionRPCService: TransactionRPCService, ledgerDataHandler: LedgerFutureDataHandler, blockDataHandler: BlockFutureDataHandler)( implicit ec: ExecutionContext) { @@ -160,7 +161,7 @@ class LedgerSynchronizerService @Inject() ( val start = System.currentTimeMillis() val result = for { block <- xsnService.getBlock(blockhash).toFutureOr - transactions <- transactionService.getTransactions(block.transactions).toFutureOr + transactions <- transactionRPCService.getTransactions(block.transactions).toFutureOr took = System.currentTimeMillis() - start _ = logger.info(s"Retrieving block = $blockhash, took $took ms") } yield (block, transactions) diff --git a/server/app/com/xsn/explorer/services/TransactionRPCService.scala b/server/app/com/xsn/explorer/services/TransactionRPCService.scala new file mode 100644 index 0000000..ec943a4 --- /dev/null +++ b/server/app/com/xsn/explorer/services/TransactionRPCService.scala @@ -0,0 +1,159 @@ +package com.xsn.explorer.services + +import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureListOps, FutureOps, OrOps} +import com.alexitc.playsonify.core.{FutureApplicationResult, FutureOr} +import com.xsn.explorer.errors.{InvalidRawTransactionError, TransactionFormatError, TransactionNotFoundError, XSNWorkQueueDepthExceeded} +import com.xsn.explorer.models.rpc.TransactionVIN +import com.xsn.explorer.models.{HexString, Transaction, TransactionDetails, TransactionId, TransactionValue} +import com.xsn.explorer.util.Extensions.FutureOrExt +import javax.inject.Inject +import org.scalactic.{Bad, Good, One, Or} +import org.slf4j.LoggerFactory +import play.api.libs.json.{JsObject, JsString, JsValue} + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.control.NonFatal + +class TransactionRPCService @Inject() ( + xsnService: XSNService)( + implicit ec: ExecutionContext) { + + private val logger = LoggerFactory.getLogger(this.getClass) + + def getRawTransaction(txidString: String): FutureApplicationResult[JsValue] = { + val result = for { + txid <- { + val maybe = TransactionId.from(txidString) + Or.from(maybe, One(TransactionFormatError)).toFutureOr + } + + transaction <- xsnService.getRawTransaction(txid).toFutureOr + } yield transaction + + result.toFuture + } + + def getTransactionDetails(txidString: String): FutureApplicationResult[TransactionDetails] = { + val result = for { + txid <- { + val maybe = TransactionId.from(txidString) + Or.from(maybe, One(TransactionFormatError)).toFutureOr + } + + transaction <- xsnService.getTransaction(txid).toFutureOr + + input <- transaction + .vin + .map(getTransactionValue) + .toFutureOr + } yield TransactionDetails.from(transaction, input) + + result.toFuture + } + + def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = { + val result = for { + tx <- xsnService.getTransaction(txid).toFutureOr + transactionVIN <- getTransactionVIN(tx.vin).toFutureOr + rpcTransaction = tx.copy(vin = transactionVIN) + } yield Transaction.fromRPC(rpcTransaction) + + result.toFuture + } + + private def getTransactionVIN(list: List[TransactionVIN]): FutureApplicationResult[List[TransactionVIN]] = { + def getVIN(vin: TransactionVIN) = { + getTransactionValue(vin) + .map { + case Good(transactionValue) => + val newVIN = vin.copy(address = Some(transactionValue.address), value = Some(transactionValue.value)) + Good(newVIN) + + case Bad(e) => Bad(e) + } + } + + def loadVINSequentially(pending: List[TransactionVIN]): FutureOr[List[TransactionVIN]] = pending match { + case x :: xs => + for { + tx <- getVIN(x).toFutureOr + next <- loadVINSequentially(xs) + } yield tx :: next + + case _ => Future.successful(Good(List.empty)).toFutureOr + } + + list + .map(getVIN) + .toFutureOr + .toFuture + .recoverWith { + case NonFatal(ex) => + logger.warn(s"Failed to load VIN, trying sequentially, error = ${ex.getMessage}") + loadVINSequentially(list).toFuture + } + } + + def getTransactions(ids: List[TransactionId]): FutureApplicationResult[List[Transaction]] = { + def loadTransactionsSlowly(pending: List[TransactionId]): FutureOr[List[Transaction]] = pending match { + case x :: xs => + for { + tx <- getTransaction(x).toFutureOr + next <- loadTransactionsSlowly(xs) + } yield tx :: next + + case _ => Future.successful(Good(List.empty)).toFutureOr + } + + ids + .map(getTransaction) + .toFutureOr + .recoverWith(XSNWorkQueueDepthExceeded) { + logger.warn("Unable to load transaction due to server overload, loading them slowly") + loadTransactionsSlowly(ids) + } + .toFuture + .recoverWith { + case NonFatal(ex) => + logger.warn(s"Unable to load transactions due to server error, loading them sequentially, error = ${ex.getMessage}") + loadTransactionsSlowly(ids).toFuture + } + } + + def sendRawTransaction(hexString: String): FutureApplicationResult[JsValue] = { + val result = for { + hex <- Or.from(HexString.from(hexString), One(InvalidRawTransactionError)).toFutureOr + _ <- xsnService.sendRawTransaction(hex).toFutureOr + } yield JsObject.empty + ("hex" -> JsString(hex.string)) + + result.toFuture + } + + private def getTransactionValue(vin: TransactionVIN): FutureApplicationResult[TransactionValue] = { + val valueMaybe = for { + value <- vin.value + address <- vin.address + } yield TransactionValue(address, value) + + valueMaybe + .map(Good(_)) + .map(Future.successful) + .getOrElse { + val txid = vin.txid + + val result = for { + tx <- xsnService.getTransaction(txid).toFutureOr + r <- { + val maybe = tx + .vout + .find(_.n == vin.voutIndex) + .flatMap(TransactionValue.from) + + Or.from(maybe, One(TransactionNotFoundError)).toFutureOr + } + } yield r + + result.toFuture + } + } +} diff --git a/server/app/com/xsn/explorer/services/TransactionService.scala b/server/app/com/xsn/explorer/services/TransactionService.scala index 0e6128d..c47df28 100644 --- a/server/app/com/xsn/explorer/services/TransactionService.scala +++ b/server/app/com/xsn/explorer/services/TransactionService.scala @@ -1,29 +1,24 @@ package com.xsn.explorer.services -import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureListOps, FutureOps, OrOps} -import com.alexitc.playsonify.core.{FutureApplicationResult, FutureOr, FuturePaginatedResult} +import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureOps, OrOps} +import com.alexitc.playsonify.core.{FutureApplicationResult, FuturePaginatedResult} import com.alexitc.playsonify.models.ordering.{OrderingCondition, OrderingError, OrderingQuery} import com.alexitc.playsonify.models.pagination.{Limit, Offset, PaginatedQuery} import com.alexitc.playsonify.validators.PaginatedQueryValidator import com.xsn.explorer.data.async.TransactionFutureDataHandler import com.xsn.explorer.errors._ import com.xsn.explorer.models._ -import com.xsn.explorer.models.rpc.TransactionVIN import com.xsn.explorer.parsers.TransactionOrderingParser -import com.xsn.explorer.util.Extensions.FutureOrExt import io.scalaland.chimney.dsl._ import javax.inject.Inject import org.scalactic._ import org.slf4j.LoggerFactory -import play.api.libs.json.{JsObject, JsString, JsValue} -import scala.concurrent.{ExecutionContext, Future} -import scala.util.control.NonFatal +import scala.concurrent.ExecutionContext class TransactionService @Inject() ( paginatedQueryValidator: PaginatedQueryValidator, transactionOrderingParser: TransactionOrderingParser, - xsnService: XSNService, transactionFutureDataHandler: TransactionFutureDataHandler)( implicit ec: ExecutionContext) { @@ -31,106 +26,6 @@ class TransactionService @Inject() ( private val maxTransactionsPerQuery = 100 - def getRawTransaction(txidString: String): FutureApplicationResult[JsValue] = { - val result = for { - txid <- { - val maybe = TransactionId.from(txidString) - Or.from(maybe, One(TransactionFormatError)).toFutureOr - } - - transaction <- xsnService.getRawTransaction(txid).toFutureOr - } yield transaction - - result.toFuture - } - - def getTransactionDetails(txidString: String): FutureApplicationResult[TransactionDetails] = { - val result = for { - txid <- { - val maybe = TransactionId.from(txidString) - Or.from(maybe, One(TransactionFormatError)).toFutureOr - } - - transaction <- xsnService.getTransaction(txid).toFutureOr - - input <- transaction - .vin - .map(getTransactionValue) - .toFutureOr - } yield TransactionDetails.from(transaction, input) - - result.toFuture - } - - def getTransaction(txid: TransactionId): FutureApplicationResult[Transaction] = { - val result = for { - tx <- xsnService.getTransaction(txid).toFutureOr - transactionVIN <- getTransactionVIN(tx.vin).toFutureOr - rpcTransaction = tx.copy(vin = transactionVIN) - } yield Transaction.fromRPC(rpcTransaction) - - result.toFuture - } - - private def getTransactionVIN(list: List[TransactionVIN]): FutureApplicationResult[List[TransactionVIN]] = { - def getVIN(vin: TransactionVIN) = { - getTransactionValue(vin) - .map { - case Good(transactionValue) => - val newVIN = vin.copy(address = Some(transactionValue.address), value = Some(transactionValue.value)) - Good(newVIN) - - case Bad(e) => Bad(e) - } - } - - def loadVINSequentially(pending: List[TransactionVIN]): FutureOr[List[TransactionVIN]] = pending match { - case x :: xs => - for { - tx <- getVIN(x).toFutureOr - next <- loadVINSequentially(xs) - } yield tx :: next - - case _ => Future.successful(Good(List.empty)).toFutureOr - } - - list - .map(getVIN) - .toFutureOr - .toFuture - .recoverWith { - case NonFatal(ex) => - logger.warn(s"Failed to load VIN, trying sequentially, error = ${ex.getMessage}") - loadVINSequentially(list).toFuture - } - } - - def getTransactions(ids: List[TransactionId]): FutureApplicationResult[List[Transaction]] = { - def loadTransactionsSlowly(pending: List[TransactionId]): FutureOr[List[Transaction]] = pending match { - case x :: xs => - for { - tx <- getTransaction(x).toFutureOr - next <- loadTransactionsSlowly(xs) - } yield tx :: next - - case _ => Future.successful(Good(List.empty)).toFutureOr - } - - ids - .map(getTransaction) - .toFutureOr - .recoverWith(XSNWorkQueueDepthExceeded) { - logger.warn("Unable to load transaction due to server overload, loading them slowly") - loadTransactionsSlowly(ids) - } - .toFuture - .recoverWith { - case NonFatal(ex) => - logger.warn(s"Unable to load transactions due to server error, loading them sequentially, error = ${ex.getMessage}") - loadTransactionsSlowly(ids).toFuture - } - } - def getTransactions( addressString: String, paginatedQuery: PaginatedQuery, @@ -201,15 +96,6 @@ class TransactionService @Inject() ( result.toFuture } - def sendRawTransaction(hexString: String): FutureApplicationResult[JsValue] = { - val result = for { - hex <- Or.from(HexString.from(hexString), One(InvalidRawTransactionError)).toFutureOr - _ <- xsnService.sendRawTransaction(hex).toFutureOr - } yield JsObject.empty + ("hex" -> JsString(hex.string)) - - result.toFuture - } - def getByBlockhash(blockhashString: String, paginatedQuery: PaginatedQuery, orderingQuery: OrderingQuery): FuturePaginatedResult[TransactionWithValues] = { val result = for { blockhash <- Or.from(Blockhash.from(blockhashString), One(BlockhashFormatError)).toFutureOr @@ -240,34 +126,6 @@ class TransactionService @Inject() ( result.toFuture } - private def getTransactionValue(vin: TransactionVIN): FutureApplicationResult[TransactionValue] = { - val valueMaybe = for { - value <- vin.value - address <- vin.address - } yield TransactionValue(address, value) - - valueMaybe - .map(Good(_)) - .map(Future.successful) - .getOrElse { - val txid = vin.txid - - val result = for { - tx <- xsnService.getTransaction(txid).toFutureOr - r <- { - val maybe = tx - .vout - .find(_.n == vin.voutIndex) - .flatMap(TransactionValue.from) - - Or.from(maybe, One(TransactionNotFoundError)).toFutureOr - } - } yield r - - result.toFuture - } - } - /** TODO: Move to another file */ private def getOrderingConditionResult(unsafeOrderingCondition: String) = { val maybe = parseOrderingCondition(unsafeOrderingCondition) diff --git a/server/app/controllers/TransactionsController.scala b/server/app/controllers/TransactionsController.scala index 5f817d7..50fe0be 100644 --- a/server/app/controllers/TransactionsController.scala +++ b/server/app/controllers/TransactionsController.scala @@ -1,26 +1,26 @@ package controllers import com.xsn.explorer.models.request.SendRawTransactionRequest -import com.xsn.explorer.services.TransactionService +import com.xsn.explorer.services.TransactionRPCService import controllers.common.{MyJsonController, MyJsonControllerComponents} import javax.inject.Inject class TransactionsController @Inject() ( - transactionService: TransactionService, + transactionRPCService: TransactionRPCService, cc: MyJsonControllerComponents) extends MyJsonController(cc) { import Context._ def getTransaction(txid: String) = public { _ => - transactionService.getTransactionDetails(txid) + transactionRPCService.getTransactionDetails(txid) } def getRawTransaction(txid: String) = public { _ => - transactionService.getRawTransaction(txid) + transactionRPCService.getRawTransaction(txid) } def sendRawTransaction() = publicInput { ctx: HasModel[SendRawTransactionRequest] => - transactionService.sendRawTransaction(ctx.model.hex) + transactionRPCService.sendRawTransaction(ctx.model.hex) } } diff --git a/server/test/com/xsn/explorer/services/LedgerSynchronizerServiceSpec.scala b/server/test/com/xsn/explorer/services/LedgerSynchronizerServiceSpec.scala index 3f93240..19199b0 100644 --- a/server/test/com/xsn/explorer/services/LedgerSynchronizerServiceSpec.scala +++ b/server/test/com/xsn/explorer/services/LedgerSynchronizerServiceSpec.scala @@ -221,12 +221,13 @@ class LedgerSynchronizerServiceSpec extends PostgresDataHandlerSpec with BeforeA val transactionService = new TransactionService( new PaginatedQueryValidator, new TransactionOrderingParser, - xsnService, new TransactionFutureDataHandler(transactionDataHandler)(Executors.databaseEC)) + val transactionRPCService = new TransactionRPCService(xsnService) new LedgerSynchronizerService( xsnService, transactionService, + transactionRPCService, new LedgerFutureDataHandler(dataHandler)(Executors.databaseEC), new BlockFutureDataHandler(blockDataHandler)(Executors.databaseEC)) }