diff --git a/server/app/com/xsn/explorer/data/TransactionDataHandler.scala b/server/app/com/xsn/explorer/data/TransactionDataHandler.scala index 6334d71..f5b29b1 100644 --- a/server/app/com/xsn/explorer/data/TransactionDataHandler.scala +++ b/server/app/com/xsn/explorer/data/TransactionDataHandler.scala @@ -4,6 +4,7 @@ import com.alexitc.playsonify.core.ApplicationResult import com.alexitc.playsonify.models.{FieldOrdering, PaginatedQuery, PaginatedResult} import com.xsn.explorer.models._ import com.xsn.explorer.models.fields.TransactionField +import org.scalactic.Every import scala.language.higherKinds @@ -21,7 +22,7 @@ trait TransactionDataHandler[F[_]] { paginatedQuery: PaginatedQuery, ordering: FieldOrdering[TransactionField]): F[PaginatedResult[TransactionWithValues]] - def getLatestTransactionBy(addresses: List[Address]): F[Map[String, String]] + def getLatestTransactionBy(addresses: Every[Address]): F[Map[String, String]] } trait TransactionBlockingDataHandler extends TransactionDataHandler[ApplicationResult] diff --git a/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala b/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala index 81cd29c..394a034 100644 --- a/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala +++ b/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala @@ -1,14 +1,13 @@ package com.xsn.explorer.data.anorm import javax.inject.Inject - import com.alexitc.playsonify.core.ApplicationResult import com.alexitc.playsonify.models.{FieldOrdering, PaginatedQuery, PaginatedResult} import com.xsn.explorer.data.TransactionBlockingDataHandler import com.xsn.explorer.data.anorm.dao.TransactionPostgresDAO import com.xsn.explorer.models._ import com.xsn.explorer.models.fields.TransactionField -import org.scalactic.Good +import org.scalactic.{Every, Good} import play.api.db.Database class TransactionPostgresDataHandler @Inject() ( @@ -46,7 +45,7 @@ class TransactionPostgresDataHandler @Inject() ( Good(result) } - def getLatestTransactionBy(addresses: List[Address]): ApplicationResult[Map[String, String]] = withConnection { implicit conn => + def getLatestTransactionBy(addresses: Every[Address]): ApplicationResult[Map[String, String]] = withConnection { implicit conn => val result = transactionPostgresDAO.getLatestTransactionBy(addresses) Good(result) diff --git a/server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala b/server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala index 081d8c0..ba8f9c8 100644 --- a/server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala +++ b/server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala @@ -1,14 +1,15 @@ package com.xsn.explorer.data.anorm.dao import java.sql.Connection -import javax.inject.Inject +import javax.inject.Inject import anorm._ import com.alexitc.playsonify.models.{Count, FieldOrdering, PaginatedQuery} import com.xsn.explorer.data.anorm.interpreters.FieldOrderingSQLInterpreter import com.xsn.explorer.data.anorm.parsers.TransactionParsers._ import com.xsn.explorer.models._ import com.xsn.explorer.models.fields.TransactionField +import org.scalactic.Every class TransactionPostgresDAO @Inject() (fieldOrderingSQLInterpreter: FieldOrderingSQLInterpreter) { @@ -184,7 +185,7 @@ class TransactionPostgresDAO @Inject() (fieldOrderingSQLInterpreter: FieldOrderi ).as(parseTransactionOutput.*).flatten } - def getLatestTransactionBy(addresses: List[Address])(implicit conn: Connection): Map[String, String] = { + def getLatestTransactionBy(addresses: Every[Address])(implicit conn: Connection): Map[String, String] = { import SqlParser._ @@ -202,7 +203,7 @@ class TransactionPostgresDAO @Inject() (fieldOrderingSQLInterpreter: FieldOrderi |HAVING address IN ({addresses}); """.stripMargin ).on( - 'addresses -> addresses.map(_.string) + 'addresses -> addresses.map(_.string).toList ).as((str("address") ~ str("txid")).map(flatten).*) result.toMap diff --git a/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala b/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala index a12a6ef..8a0dc42 100644 --- a/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala +++ b/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala @@ -1,13 +1,13 @@ package com.xsn.explorer.data.async import javax.inject.Inject - import com.alexitc.playsonify.core.{FutureApplicationResult, FuturePaginatedResult} import com.alexitc.playsonify.models.{FieldOrdering, PaginatedQuery, PaginatedResult} import com.xsn.explorer.data.{TransactionBlockingDataHandler, TransactionDataHandler} import com.xsn.explorer.executors.DatabaseExecutionContext import com.xsn.explorer.models._ import com.xsn.explorer.models.fields.TransactionField +import org.scalactic.Every import scala.concurrent.Future @@ -36,7 +36,7 @@ class TransactionFutureDataHandler @Inject() ( blockingDataHandler.getByBlockhash(blockhash, paginatedQuery, ordering) } - override def getLatestTransactionBy(addresses: List[Address]): FutureApplicationResult[Map[String, String]] = Future { + override def getLatestTransactionBy(addresses: Every[Address]): FutureApplicationResult[Map[String, String]] = Future { blockingDataHandler.getLatestTransactionBy(addresses) } } diff --git a/server/app/com/xsn/explorer/services/TransactionService.scala b/server/app/com/xsn/explorer/services/TransactionService.scala index 3e1efcf..fa7200c 100644 --- a/server/app/com/xsn/explorer/services/TransactionService.scala +++ b/server/app/com/xsn/explorer/services/TransactionService.scala @@ -1,7 +1,6 @@ package com.xsn.explorer.services import javax.inject.Inject - import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureListOps, FutureOps, OrOps} import com.alexitc.playsonify.core.{FutureApplicationResult, FuturePaginatedResult} import com.alexitc.playsonify.models.{OrderingQuery, PaginatedQuery} @@ -11,7 +10,7 @@ import com.xsn.explorer.errors._ import com.xsn.explorer.models._ import com.xsn.explorer.models.rpc.TransactionVIN import com.xsn.explorer.parsers.TransactionOrderingParser -import org.scalactic.{Bad, Good, One, Or} +import org.scalactic._ import play.api.libs.json.{JsObject, JsString, JsValue} import scala.concurrent.{ExecutionContext, Future} @@ -115,7 +114,7 @@ class TransactionService @Inject() ( result.toFuture } - def getLatestTransactionBy(addresses: List[Address]): FutureApplicationResult[Map[String, String]] = { + def getLatestTransactionBy(addresses: Every[Address]): FutureApplicationResult[Map[String, String]] = { transactionFutureDataHandler.getLatestTransactionBy(addresses) } diff --git a/server/app/controllers/TransactionsController.scala b/server/app/controllers/TransactionsController.scala index 16351b1..1dc886f 100644 --- a/server/app/controllers/TransactionsController.scala +++ b/server/app/controllers/TransactionsController.scala @@ -1,18 +1,19 @@ package controllers import javax.inject.Inject - import com.alexitc.playsonify.models.PublicContextWithModel import com.xsn.explorer.models.Address import com.xsn.explorer.models.request.SendRawTransactionRequest import com.xsn.explorer.services.TransactionService -import controllers.common.{MyJsonController, MyJsonControllerComponents} +import controllers.common.{Codecs,MyJsonController, MyJsonControllerComponents} +import org.scalactic.Every class TransactionsController @Inject() ( transactionService: TransactionService, cc: MyJsonControllerComponents) extends MyJsonController(cc) { + import Codecs._ def getTransaction(txid: String) = publicNoInput { _ => transactionService.getTransactionDetails(txid) } @@ -25,7 +26,7 @@ class TransactionsController @Inject() ( transactionService.sendRawTransaction(ctx.model.hex) } - def getLatestByAddresses() = publicWithInput { ctx: PublicContextWithModel[List[Address]] => + def getLatestByAddresses() = publicWithInput { ctx: PublicContextWithModel[Every[Address]] => transactionService.getLatestTransactionBy(ctx.model) } } diff --git a/server/app/controllers/common/Codecs.scala b/server/app/controllers/common/Codecs.scala new file mode 100644 index 0000000..4346198 --- /dev/null +++ b/server/app/controllers/common/Codecs.scala @@ -0,0 +1,19 @@ +package controllers.common + +import org.scalactic.Every +import play.api.libs.json._ + +object Codecs { + + implicit def everyReads[T](implicit readsT: Reads[T]): Reads[Every[T]] = Reads[Every[T]] { json => + json + .validate[List[T]] + .flatMap { list => + Every.from(list) + .map(JsSuccess.apply(_)) + .getOrElse { + JsError.apply("A non-empty list is expected") + } + } + } +} \ No newline at end of file diff --git a/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala b/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala index ebbabfb..b0e2b71 100644 --- a/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala +++ b/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala @@ -11,7 +11,7 @@ import com.xsn.explorer.helpers.{BlockLoader, TransactionLoader} import com.xsn.explorer.models._ import com.xsn.explorer.models.fields.TransactionField import com.xsn.explorer.models.rpc.Block -import org.scalactic.{Good, One, Or} +import org.scalactic.{Every, Good, One, Or} import org.scalatest.BeforeAndAfter class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with BeforeAndAfter { @@ -307,7 +307,7 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be "XdJnCKYNwzCz8ATv8Eu75gonaHyfr9qXg9" -> "1e591eae200f719344fc5df0c4286e3fb191fb8a645bdf054f9b36a856fce41e" ) - val addresses = List( + val addresses = Every( createAddress("XdJnCKYNwzCz8ATv8Eu75gonaHyfr9qXg9"), createAddress("XcqpUChZhNkVDgQqFF9U4DdewDGUMWwG53"), createAddress("XcqpUChZhNkVDgQqFF9U4DdewDGUMWwG54"), diff --git a/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala b/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala index 2f5f4c8..8d8b6c2 100644 --- a/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala +++ b/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala @@ -5,6 +5,7 @@ import com.alexitc.playsonify.models.{FieldOrdering, PaginatedQuery, PaginatedRe import com.xsn.explorer.data.TransactionBlockingDataHandler import com.xsn.explorer.models._ import com.xsn.explorer.models.fields.TransactionField +import org.scalactic.Every class TransactionDummyDataHandler extends TransactionBlockingDataHandler { @@ -14,5 +15,5 @@ class TransactionDummyDataHandler extends TransactionBlockingDataHandler { override def getByBlockhash(blockhash: Blockhash, paginatedQuery: PaginatedQuery, ordering: FieldOrdering[TransactionField]): ApplicationResult[PaginatedResult[TransactionWithValues]] = ??? - override def getLatestTransactionBy(addresses: List[Address]): ApplicationResult[Map[String, String]] = ??? + override def getLatestTransactionBy(addresses: Every[Address]): ApplicationResult[Map[String, String]] = ??? } diff --git a/server/test/controllers/TransactionsControllerSpec.scala b/server/test/controllers/TransactionsControllerSpec.scala index 779d93a..ae9821d 100644 --- a/server/test/controllers/TransactionsControllerSpec.scala +++ b/server/test/controllers/TransactionsControllerSpec.scala @@ -1,16 +1,17 @@ package controllers import com.alexitc.playsonify.PublicErrorRenderer -import com.alexitc.playsonify.core.FutureApplicationResult +import com.alexitc.playsonify.core.{ApplicationResult, FutureApplicationResult} +import com.xsn.explorer.data.TransactionBlockingDataHandler import com.xsn.explorer.errors.TransactionNotFoundError -import com.xsn.explorer.helpers.{DataHelper, DummyXSNService, TransactionLoader} +import com.xsn.explorer.helpers.{DataHelper, DummyXSNService, TransactionDummyDataHandler, TransactionLoader} import com.xsn.explorer.models._ import com.xsn.explorer.models.rpc.Transaction import com.xsn.explorer.services.XSNService import controllers.common.MyAPISpec -import org.scalactic.{Bad, Good} +import org.scalactic.{Bad, Every, Good} import play.api.inject.bind -import play.api.libs.json.JsValue +import play.api.libs.json.{JsValue, Json} import play.api.test.Helpers._ import scala.concurrent.Future @@ -23,6 +24,10 @@ class TransactionsControllerSpec extends MyAPISpec { val nonCoinbaseTx = TransactionLoader.get("0834641a7d30d8a2d2b451617599670445ee94ed7736e146c13be260c576c641") val nonCoinbasePreviousTx = TransactionLoader.get("585cec5009c8ca19e83e33d282a6a8de65eb2ca007b54d6572167703768967d9") val severalInputsTx = TransactionLoader.get("a3c43d22bbba31a6e5c00f565cb9c5a1a365407df4cc90efa8a865656b52c0eb") + val firstAddress = createAddress("fygsydgfygsdyfgsdyg") + val secondAddress = createAddress("56wedf5wedweedw") + val firstTxId = DataHelper.createTransactionId("a3c43d223658a8656a31a6e5c407df4bbb0f565cb9c5a1acc90efa056b52c0eb") + val secondTxId = DataHelper.createTransactionId("8a865656b5a3c43d22b00f565cb9c5a1a3bba31a6e5c65407df4cc90efa2c0eb") val customXSNService = new DummyXSNService { val map = Map( @@ -54,9 +59,23 @@ class TransactionsControllerSpec extends MyAPISpec { } } + val transactionDataHandler = new TransactionDummyDataHandler { + + val map: Map[Address, TransactionId] = Map(firstAddress -> firstTxId, secondAddress -> secondTxId) + + override def getLatestTransactionBy(addresses: Every[Address]): ApplicationResult[Map[String, String]] = { + val result = map + .filterKeys(addresses contains _) + .map( x => x._1.string -> x._2.string ) + + Good(result) + } + } + override val application = guiceApplicationBuilder - .overrides(bind[XSNService].to(customXSNService)) - .build() + .overrides(bind[XSNService].to(customXSNService)) + .overrides(bind[TransactionBlockingDataHandler].to(transactionDataHandler)) + .build() "GET /transactions/:txid" should { def url(txid: String) = s"/transactions/$txid" @@ -205,4 +224,39 @@ class TransactionsControllerSpec extends MyAPISpec { json mustEqual expected } } + + "POST /transactions/latest" should { + def url = s"/transactions/latest" + + "return the latest transactions for the addresses" in { + + val body = List(firstAddress.string, secondAddress.string, "3rdaddress") + .map(x => s""" "$x" """) + .mkString("[", ",", "]") + val expected = Json.obj(firstAddress.string -> firstTxId, secondAddress.string -> secondTxId) + + val response = POST(url, Some(body)) + + status(response) mustEqual OK + val json = contentAsJson(response) + json mustEqual expected + } + + "fail while passing no addresses" in { + val body = """[]""" + val response = POST(url, Some(body)) + + status(response) mustEqual BAD_REQUEST + + val json = contentAsJson(response) + val errorList = (json \ "errors").as[List[JsValue]] + + errorList.size mustEqual 1 + val error = errorList.head + + (error \ "type").as[String] mustEqual PublicErrorRenderer.FieldValidationErrorType + (error \ "field").as[String].nonEmpty mustEqual false + (error \ "message").as[String].nonEmpty mustEqual true + } + } }