From b4a4577186a3978dfe76a2d681cc9f1bc8cea96a Mon Sep 17 00:00:00 2001 From: Alexis Hernandez Date: Sun, 30 Sep 2018 17:38:06 -0600 Subject: [PATCH] server: Add "GET /blocks/:blockhash/transactions" (#19) --- .../services/TransactionService.scala | 16 +++++- server/app/controllers/BlocksController.scala | 10 +++- server/conf/routes | 7 +-- .../controllers/BlocksControllerSpec.scala | 54 ++++++++++++++++++- 4 files changed, 79 insertions(+), 8 deletions(-) diff --git a/server/app/com/xsn/explorer/services/TransactionService.scala b/server/app/com/xsn/explorer/services/TransactionService.scala index ca8f29c..bd8aa50 100644 --- a/server/app/com/xsn/explorer/services/TransactionService.scala +++ b/server/app/com/xsn/explorer/services/TransactionService.scala @@ -7,7 +7,7 @@ import com.alexitc.playsonify.core.{FutureApplicationResult, FuturePaginatedResu import com.alexitc.playsonify.models.{OrderingQuery, PaginatedQuery} import com.alexitc.playsonify.validators.PaginatedQueryValidator import com.xsn.explorer.data.async.TransactionFutureDataHandler -import com.xsn.explorer.errors.{AddressFormatError, InvalidRawTransactionError, TransactionFormatError, TransactionNotFoundError} +import com.xsn.explorer.errors._ import com.xsn.explorer.models._ import com.xsn.explorer.models.rpc.TransactionVIN import com.xsn.explorer.parsers.TransactionOrderingParser @@ -23,6 +23,8 @@ class TransactionService @Inject() ( transactionFutureDataHandler: TransactionFutureDataHandler)( implicit ec: ExecutionContext) { + private val maxTransactionsPerQuery = 100 + def getRawTransaction(txidString: String): FutureApplicationResult[JsValue] = { val result = for { txid <- { @@ -85,7 +87,7 @@ class TransactionService @Inject() ( Or.from(maybe, One(AddressFormatError)).toFutureOr } - paginatedQuery <- paginatedQueryValidator.validate(paginatedQuery, 100).toFutureOr + paginatedQuery <- paginatedQueryValidator.validate(paginatedQuery, maxTransactionsPerQuery).toFutureOr ordering <- transactionOrderingParser.from(orderingQuery).toFutureOr transactions <- transactionFutureDataHandler.getBy(address, paginatedQuery, ordering).toFutureOr } yield transactions @@ -102,6 +104,16 @@ class TransactionService @Inject() ( result.toFuture } + def getByBlockhash(blockhashString: String, paginatedQuery: PaginatedQuery, orderingQuery: OrderingQuery): FuturePaginatedResult[TransactionWithValues] = { + val result = for { + blockhash <- Or.from(Blockhash.from(blockhashString), One(BlockhashFormatError)).toFutureOr + validatedQuery <- paginatedQueryValidator.validate(paginatedQuery, maxTransactionsPerQuery).toFutureOr + order <- transactionOrderingParser.from(orderingQuery).toFutureOr + r <- transactionFutureDataHandler.getByBlockhash(blockhash, validatedQuery, order).toFutureOr + } yield r + + result.toFuture + } private def getTransactionValue(vin: TransactionVIN): FutureApplicationResult[TransactionValue] = { val valueMaybe = for { value <- vin.value diff --git a/server/app/controllers/BlocksController.scala b/server/app/controllers/BlocksController.scala index 9c75833..ac2c45b 100644 --- a/server/app/controllers/BlocksController.scala +++ b/server/app/controllers/BlocksController.scala @@ -2,14 +2,16 @@ package controllers import javax.inject.Inject +import com.alexitc.playsonify.models.{Limit, Offset, OrderingQuery, PaginatedQuery} import com.xsn.explorer.models.Height -import com.xsn.explorer.services.BlockService +import com.xsn.explorer.services.{BlockService, TransactionService} import controllers.common.{MyJsonController, MyJsonControllerComponents} import scala.util.Try class BlocksController @Inject() ( blockService: BlockService, + transactionService: TransactionService, cc: MyJsonControllerComponents) extends MyJsonController(cc) { @@ -35,4 +37,10 @@ class BlocksController @Inject() ( .map(blockService.getRawBlock) .getOrElse(blockService.getRawBlock(query)) } + + def getTransactions(blockhash: String, offset: Int, limit: Int, orderBy: String) = publicNoInput { _ => + val query = PaginatedQuery(Offset(offset), Limit(limit)) + val ordering = OrderingQuery(orderBy) + transactionService.getByBlockhash(blockhash, query, ordering) + } } diff --git a/server/conf/routes b/server/conf/routes index 716e274..23f3a7d 100644 --- a/server/conf/routes +++ b/server/conf/routes @@ -13,9 +13,10 @@ GET /addresses/:address controllers.AddressesController.getBy( GET /addresses/:address/transactions controllers.AddressesController.getTransactions(address: String, offset: Int ?= 0, limit: Int ?= 10, orderBy: String ?= "") GET /addresses/:address/utxos controllers.AddressesController.getUnspentOutputs(address: String) -GET /blocks controllers.BlocksController.getLatestBlocks() -GET /blocks/:query controllers.BlocksController.getDetails(query: String) -GET /blocks/:query/raw controllers.BlocksController.getRawBlock(query: String) +GET /blocks controllers.BlocksController.getLatestBlocks() +GET /blocks/:query controllers.BlocksController.getDetails(query: String) +GET /blocks/:query/raw controllers.BlocksController.getRawBlock(query: String) +GET /blocks/:blockhash/transactions controllers.BlocksController.getTransactions(blockhash: String, offset: Int ?= 0, limit: Int ?= 10, orderBy: String ?= "") GET /stats controllers.StatisticsController.getStatus() diff --git a/server/test/controllers/BlocksControllerSpec.scala b/server/test/controllers/BlocksControllerSpec.scala index bd3b1df..b0bcaa2 100644 --- a/server/test/controllers/BlocksControllerSpec.scala +++ b/server/test/controllers/BlocksControllerSpec.scala @@ -1,10 +1,13 @@ package controllers import com.alexitc.playsonify.PublicErrorRenderer -import com.alexitc.playsonify.core.FutureApplicationResult +import com.alexitc.playsonify.core.{ApplicationResult, FutureApplicationResult} +import com.alexitc.playsonify.models.{Count, FieldOrdering, PaginatedQuery, PaginatedResult} +import com.xsn.explorer.data.TransactionBlockingDataHandler import com.xsn.explorer.errors.{BlockNotFoundError, TransactionNotFoundError} -import com.xsn.explorer.helpers.{BlockLoader, DummyXSNService, TransactionLoader} +import com.xsn.explorer.helpers.{BlockLoader, DummyXSNService, TransactionDummyDataHandler, TransactionLoader} import com.xsn.explorer.models._ +import com.xsn.explorer.models.fields.TransactionField import com.xsn.explorer.models.rpc.{Block, Transaction} import com.xsn.explorer.services.XSNService import controllers.common.MyAPISpec @@ -106,8 +109,38 @@ class BlocksControllerSpec extends MyAPISpec { } } + val transactionDataHandler = new TransactionDummyDataHandler { + // TODO: Handle ordering + override def getByBlockhash(blockhash: Blockhash, paginatedQuery: PaginatedQuery, ordering: FieldOrdering[TransactionField]): ApplicationResult[PaginatedResult[TransactionWithValues]] = { + val transactions = BlockLoader + .get(blockhash.string) + .transactions + .map(_.string) + .map(TransactionLoader.get) + .map { tx => + TransactionWithValues( + id = tx.id, + blockhash = blockhash, + time = tx.time, + size = tx.size, + sent = tx.vin.flatMap(_.value).sum, + received = tx.vout.map(_.value).sum + ) + } + + val page = PaginatedResult( + paginatedQuery.offset, + paginatedQuery.limit, + Count(transactions.size), + transactions.drop(paginatedQuery.offset.int).take(paginatedQuery.limit.int)) + + Good(page) + } + } + override val application = guiceApplicationBuilder .overrides(bind[XSNService].to(customXSNService)) + .overrides(bind[TransactionBlockingDataHandler].to(transactionDataHandler)) .build() "GET /blocks/:query" should { @@ -394,4 +427,21 @@ class BlocksControllerSpec extends MyAPISpec { json mustEqual BlockLoader.json(block.hash.string) } } + + "GET /blocks/:blockhash/transactions" should { + "return the transactions for the given block" in { + val blockhash = "000003fb382f6892ae96594b81aa916a8923c70701de4e7054aac556c7271ef7" + val response = GET(s"/blocks/$blockhash/transactions?offset=0&limit=5&orderBy=time:desc") + + status(response) mustEqual OK + + val json = contentAsJson(response) + (json \ "total").as[Int] mustEqual 1 + (json \ "offset").as[Int] mustEqual 0 + (json \ "limit").as[Int] mustEqual 5 + + val data = (json \ "data").as[List[JsValue]] + data.size mustEqual 1 + } + } }