From 2798b528ed330bee4c5c8f10cc763bdb4c35e476 Mon Sep 17 00:00:00 2001 From: Alexis Hernandez Date: Fri, 22 Feb 2019 18:13:04 -0700 Subject: [PATCH] server: Add endpoint "GET /blocks/headers" --- infra/deployment/config/ltc-routes | 2 ++ .../xsn/explorer/data/BlockDataHandler.scala | 6 ++-- .../data/anorm/BlockPostgresDataHandler.scala | 16 ++++++++- .../data/anorm/dao/BlockPostgresDAO.scala | 35 ++++++++++++++++++- .../data/anorm/parsers/BlockParsers.scala | 7 +++- .../data/async/BlockFutureDataHandler.scala | 7 +++- .../models/persisted/BlockHeader.scala | 16 +++++++++ .../xsn/explorer/services/BlockService.scala | 29 +++++++++++++-- server/app/controllers/BlocksController.scala | 4 +++ server/conf/routes | 2 ++ .../LedgerSynchronizerServiceSpec.scala | 7 +++- 11 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 server/app/com/xsn/explorer/models/persisted/BlockHeader.scala diff --git a/infra/deployment/config/ltc-routes b/infra/deployment/config/ltc-routes index 96ff2b1..8d61667 100644 --- a/infra/deployment/config/ltc-routes +++ b/infra/deployment/config/ltc-routes @@ -14,6 +14,8 @@ GET /addresses/:address controllers.AddressesContro GET /v2/addresses/:address/transactions controllers.AddressesController.getLightWalletTransactions(address: String, limit: Int ?= 10, lastSeenTxid: Option[String], order: String ?= "desc") GET /blocks controllers.BlocksController.getLatestBlocks() +GET /blocks/headers controllers.BlocksController.getBlockHeaders(lastSeenHash: Option[String], limit: Int ?= 10) + GET /blocks/:query controllers.BlocksController.getDetails(query: String) GET /blocks/:query/raw controllers.BlocksController.getRawBlock(query: String) GET /v2/blocks/:blockhash/transactions controllers.BlocksController.getTransactionsV2(blockhash: String, limit: Int ?= 10, lastSeenTxid: Option[String]) diff --git a/server/app/com/xsn/explorer/data/BlockDataHandler.scala b/server/app/com/xsn/explorer/data/BlockDataHandler.scala index 679c8a9..95a9954 100644 --- a/server/app/com/xsn/explorer/data/BlockDataHandler.scala +++ b/server/app/com/xsn/explorer/data/BlockDataHandler.scala @@ -2,9 +2,9 @@ package com.xsn.explorer.data import com.alexitc.playsonify.core.ApplicationResult import com.alexitc.playsonify.models.ordering.FieldOrdering -import com.alexitc.playsonify.models.pagination.{PaginatedQuery, PaginatedResult} +import com.alexitc.playsonify.models.pagination.{Limit, PaginatedQuery, PaginatedResult} import com.xsn.explorer.models.fields.BlockField -import com.xsn.explorer.models.persisted.Block +import com.xsn.explorer.models.persisted.{Block, BlockHeader} import com.xsn.explorer.models.values.{Blockhash, Height} import scala.language.higherKinds @@ -24,6 +24,8 @@ trait BlockDataHandler[F[_]] { def getLatestBlock(): F[Block] def getFirstBlock(): F[Block] + + def getHeaders(limit: Limit, lastSeenHash: Option[Blockhash]): F[List[BlockHeader]] } trait BlockBlockingDataHandler extends BlockDataHandler[ApplicationResult] diff --git a/server/app/com/xsn/explorer/data/anorm/BlockPostgresDataHandler.scala b/server/app/com/xsn/explorer/data/anorm/BlockPostgresDataHandler.scala index 7b88137..7e2eaf6 100644 --- a/server/app/com/xsn/explorer/data/anorm/BlockPostgresDataHandler.scala +++ b/server/app/com/xsn/explorer/data/anorm/BlockPostgresDataHandler.scala @@ -2,12 +2,13 @@ package com.xsn.explorer.data.anorm import com.alexitc.playsonify.core.ApplicationResult import com.alexitc.playsonify.models.ordering.FieldOrdering +import com.alexitc.playsonify.models.pagination import com.alexitc.playsonify.models.pagination.{PaginatedQuery, PaginatedResult} import com.xsn.explorer.data.BlockBlockingDataHandler import com.xsn.explorer.data.anorm.dao.BlockPostgresDAO import com.xsn.explorer.errors._ import com.xsn.explorer.models.fields.BlockField -import com.xsn.explorer.models.persisted.Block +import com.xsn.explorer.models.persisted.{Block, BlockHeader} import com.xsn.explorer.models.values.{Blockhash, Height} import javax.inject.Inject import org.scalactic.{Good, One, Or} @@ -54,4 +55,17 @@ class BlockPostgresDataHandler @Inject() ( val maybe = blockPostgresDAO.getFirstBlock Or.from(maybe, One(BlockNotFoundError)) } + + override def getHeaders( + limit: pagination.Limit, + lastSeenHash: Option[Blockhash]): ApplicationResult[List[BlockHeader]] = withConnection { implicit conn => + + val result = lastSeenHash + .map { hash => + blockPostgresDAO.getHeaders(hash, limit) + } + .getOrElse { blockPostgresDAO.getHeaders(limit) } + + Good(result) + } } diff --git a/server/app/com/xsn/explorer/data/anorm/dao/BlockPostgresDAO.scala b/server/app/com/xsn/explorer/data/anorm/dao/BlockPostgresDAO.scala index f442345..8d100be 100644 --- a/server/app/com/xsn/explorer/data/anorm/dao/BlockPostgresDAO.scala +++ b/server/app/com/xsn/explorer/data/anorm/dao/BlockPostgresDAO.scala @@ -8,7 +8,7 @@ import com.alexitc.playsonify.models.pagination.{Count, Limit, Offset, Paginated import com.alexitc.playsonify.sql.FieldOrderingSQLInterpreter import com.xsn.explorer.data.anorm.parsers.BlockParsers._ import com.xsn.explorer.models.fields.BlockField -import com.xsn.explorer.models.persisted.Block +import com.xsn.explorer.models.persisted.{Block, BlockHeader} import com.xsn.explorer.models.values.{Blockhash, Height} import javax.inject.Inject @@ -150,4 +150,37 @@ class BlockPostgresDAO @Inject() (fieldOrderingSQLInterpreter: FieldOrderingSQLI val ordering = FieldOrdering(BlockField.Height, OrderingCondition.AscendingOrder) getBy(query, ordering).headOption } + + def getHeaders(limit: Limit)(implicit conn: Connection): List[BlockHeader] = { + SQL( + """ + |SELECT blockhash, previous_blockhash, merkle_root, height, time + |FROM blocks + |ORDER BY height + |LIMIT {limit} + """.stripMargin + ).on( + 'limit -> limit.int + ).as(parseHeader.*) + } + + def getHeaders(lastSeenHash: Blockhash, limit: Limit)(implicit conn: Connection): List[BlockHeader] = { + SQL( + """ + |WITH CTE AS ( + | SELECT height as lastSeenHeight + | FROM blocks + | WHERE blockhash = {lastSeenHash} + |) + |SELECT b.blockhash, b.previous_blockhash, b.merkle_root, b.height, b.time + |FROM CTE CROSS JOIN blocks b + |WHERE b.height > lastSeenHeight + |ORDER BY height + |LIMIT {limit} + """.stripMargin + ).on( + 'lastSeenHash -> lastSeenHash.string, + 'limit -> limit.int + ).as(parseHeader.*) + } } diff --git a/server/app/com/xsn/explorer/data/anorm/parsers/BlockParsers.scala b/server/app/com/xsn/explorer/data/anorm/parsers/BlockParsers.scala index ba85ffd..bc40337 100644 --- a/server/app/com/xsn/explorer/data/anorm/parsers/BlockParsers.scala +++ b/server/app/com/xsn/explorer/data/anorm/parsers/BlockParsers.scala @@ -3,7 +3,7 @@ package com.xsn.explorer.data.anorm.parsers import anorm.SqlParser._ import anorm._ import com.xsn.explorer.models._ -import com.xsn.explorer.models.persisted.Block +import com.xsn.explorer.models.persisted.{Block, BlockHeader} import com.xsn.explorer.models.values._ object BlockParsers { @@ -90,4 +90,9 @@ object BlockParsers { extractionMethod = extractionMethod ) } + + val parseHeader = (parseBlockhash ~ parsePreviousBlockhash.? ~ parseMerkleRoot ~ parseHeight ~ parseTime).map { + case blockhash ~ previousBlockhash ~ merkleRoot ~ height ~ time => + BlockHeader(blockhash, previousBlockhash, merkleRoot, height, time) + } } diff --git a/server/app/com/xsn/explorer/data/async/BlockFutureDataHandler.scala b/server/app/com/xsn/explorer/data/async/BlockFutureDataHandler.scala index cff8a19..71fec53 100644 --- a/server/app/com/xsn/explorer/data/async/BlockFutureDataHandler.scala +++ b/server/app/com/xsn/explorer/data/async/BlockFutureDataHandler.scala @@ -2,11 +2,12 @@ package com.xsn.explorer.data.async import com.alexitc.playsonify.core.{FutureApplicationResult, FuturePaginatedResult} import com.alexitc.playsonify.models.ordering.FieldOrdering +import com.alexitc.playsonify.models.pagination import com.alexitc.playsonify.models.pagination.PaginatedQuery import com.xsn.explorer.data.{BlockBlockingDataHandler, BlockDataHandler} import com.xsn.explorer.executors.DatabaseExecutionContext import com.xsn.explorer.models.fields.BlockField -import com.xsn.explorer.models.persisted.Block +import com.xsn.explorer.models.persisted.{Block, BlockHeader} import com.xsn.explorer.models.values.{Blockhash, Height} import javax.inject.Inject @@ -40,4 +41,8 @@ class BlockFutureDataHandler @Inject() ( override def getFirstBlock(): FutureApplicationResult[Block] = Future { blockBlockingDataHandler.getFirstBlock() } + + override def getHeaders(limit: pagination.Limit, lastSeenHash: Option[Blockhash]): FutureApplicationResult[List[BlockHeader]] = Future { + blockBlockingDataHandler.getHeaders(limit, lastSeenHash) + } } diff --git a/server/app/com/xsn/explorer/models/persisted/BlockHeader.scala b/server/app/com/xsn/explorer/models/persisted/BlockHeader.scala new file mode 100644 index 0000000..9ac8a05 --- /dev/null +++ b/server/app/com/xsn/explorer/models/persisted/BlockHeader.scala @@ -0,0 +1,16 @@ +package com.xsn.explorer.models.persisted + +import com.xsn.explorer.models.values._ +import play.api.libs.json.{Json, Writes} + +case class BlockHeader( + hash: Blockhash, + previousBlockhash: Option[Blockhash], + merkleRoot: Blockhash, + height: Height, + time: Long) + +object BlockHeader { + + implicit val writes: Writes[BlockHeader] = Json.writes[BlockHeader] +} \ No newline at end of file diff --git a/server/app/com/xsn/explorer/services/BlockService.scala b/server/app/com/xsn/explorer/services/BlockService.scala index 9b0cf19..c69c046 100644 --- a/server/app/com/xsn/explorer/services/BlockService.scala +++ b/server/app/com/xsn/explorer/services/BlockService.scala @@ -2,24 +2,49 @@ package com.xsn.explorer.services import com.alexitc.playsonify.core.FutureApplicationResult import com.alexitc.playsonify.core.FutureOr.Implicits.{FutureOps, OrOps} -import com.xsn.explorer.errors.BlockRewardsNotFoundError +import com.alexitc.playsonify.models.pagination.{Limit, Offset, PaginatedQuery} +import com.alexitc.playsonify.validators.PaginatedQueryValidator +import com.xsn.explorer.data.async.BlockFutureDataHandler +import com.xsn.explorer.errors.{BlockRewardsNotFoundError, BlockhashFormatError} import com.xsn.explorer.models._ import com.xsn.explorer.models.rpc.{Block, TransactionVIN} import com.xsn.explorer.models.values.{Blockhash, Height} import com.xsn.explorer.services.logic.{BlockLogic, TransactionLogic} import com.xsn.explorer.util.Extensions.FutureOrExt import javax.inject.Inject -import org.scalactic.{Bad, Good} +import org.scalactic.{Bad, Good, One, Or} import play.api.libs.json.JsValue import scala.concurrent.{ExecutionContext, Future} class BlockService @Inject() ( xsnService: XSNService, + blockDataHandler: BlockFutureDataHandler, + paginatedQueryValidator: PaginatedQueryValidator, blockLogic: BlockLogic, transactionLogic: TransactionLogic)( implicit ec: ExecutionContext) { + private val maxHeadersPerQuery = 100 + + def getBlockHeaders(limit: Limit, lastSeenHashString: Option[String]): FutureApplicationResult[WrappedResult[List[persisted.BlockHeader]]] = { + val result = for { + lastSeenHash <- { + lastSeenHashString + .map(Blockhash.from) + .map { txid => Or.from(txid, One(BlockhashFormatError)).map(Option.apply) } + .getOrElse(Good(Option.empty)) + .toFutureOr + } + + _ <- paginatedQueryValidator.validate(PaginatedQuery(Offset(0), limit), maxHeadersPerQuery).toFutureOr + + headers <- blockDataHandler.getHeaders(limit, lastSeenHash).toFutureOr + } yield WrappedResult(headers) + + result.toFuture + } + def getRawBlock(blockhashString: String): FutureApplicationResult[JsValue] = { val result = for { blockhash <- blockLogic diff --git a/server/app/controllers/BlocksController.scala b/server/app/controllers/BlocksController.scala index 9d6d310..a048487 100644 --- a/server/app/controllers/BlocksController.scala +++ b/server/app/controllers/BlocksController.scala @@ -21,6 +21,10 @@ class BlocksController @Inject() ( blockService.getLatestBlocks() } + def getBlockHeaders(lastSeenHash: Option[String], limit: Int) = public { _ => + blockService.getBlockHeaders(Limit(limit), lastSeenHash) + } + /** * Try to retrieve a block by height, in case the query argument * is not a valid height, we assume it might be a blockhash and try to diff --git a/server/conf/routes b/server/conf/routes index c9f7552..b93a2d4 100644 --- a/server/conf/routes +++ b/server/conf/routes @@ -16,6 +16,8 @@ GET /v2/addresses/:address/transactions controllers.AddressesContro GET /addresses/:address/utxos controllers.AddressesController.getUnspentOutputs(address: String) GET /blocks controllers.BlocksController.getLatestBlocks() +GET /blocks/headers controllers.BlocksController.getBlockHeaders(lastSeenHash: Option[String], limit: Int ?= 10) + 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 ?= "") diff --git a/server/test/com/xsn/explorer/services/LedgerSynchronizerServiceSpec.scala b/server/test/com/xsn/explorer/services/LedgerSynchronizerServiceSpec.scala index 6097059..597f0a7 100644 --- a/server/test/com/xsn/explorer/services/LedgerSynchronizerServiceSpec.scala +++ b/server/test/com/xsn/explorer/services/LedgerSynchronizerServiceSpec.scala @@ -215,7 +215,12 @@ class LedgerSynchronizerServiceSpec extends PostgresDataHandlerSpec with BeforeA new TransactionOrderingParser, new TransactionFutureDataHandler(transactionDataHandler)(Executors.databaseEC)) - val blockService = new BlockService(xsnService, new BlockLogic, new TransactionLogic) + val blockService = new BlockService( + xsnService, + new BlockFutureDataHandler(blockDataHandler)(Executors.databaseEC), + new PaginatedQueryValidator, + new BlockLogic, + new TransactionLogic) val transactionRPCService = new TransactionRPCService(xsnService) new LedgerSynchronizerService( xsnService,