Skip to content

Commit

Permalink
server: Add "GET /blocks/:blockhash/transactions" (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexITC committed Sep 30, 2018
1 parent 11e25da commit b4a4577
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 8 deletions.
16 changes: 14 additions & 2 deletions server/app/com/xsn/explorer/services/TransactionService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <- {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 9 additions & 1 deletion server/app/controllers/BlocksController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand All @@ -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)
}
}
7 changes: 4 additions & 3 deletions server/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
54 changes: 52 additions & 2 deletions server/test/controllers/BlocksControllerSpec.scala
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
}

0 comments on commit b4a4577

Please sign in to comment.