diff --git a/server/app/com/xsn/explorer/data/TransactionDataHandler.scala b/server/app/com/xsn/explorer/data/TransactionDataHandler.scala index 492cccb..a6dd409 100644 --- a/server/app/com/xsn/explorer/data/TransactionDataHandler.scala +++ b/server/app/com/xsn/explorer/data/TransactionDataHandler.scala @@ -15,6 +15,11 @@ trait TransactionDataHandler[F[_]] { ordering: FieldOrdering[TransactionField]): F[PaginatedResult[TransactionWithValues]] def getUnspentOutputs(address: Address): F[List[Transaction.Output]] + + def getByBlockhash( + blockhash: Blockhash, + paginatedQuery: PaginatedQuery, + ordering: FieldOrdering[TransactionField]): F[PaginatedResult[TransactionWithValues]] } 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 b6d340b..a6d3b0f 100644 --- a/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala +++ b/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala @@ -29,8 +29,20 @@ class TransactionPostgresDataHandler @Inject() ( Good(result) } - def getUnspentOutputs(address: Address): ApplicationResult[List[Transaction.Output]] = withConnection { implicit conn => + override def getUnspentOutputs(address: Address): ApplicationResult[List[Transaction.Output]] = withConnection { implicit conn => val result = transactionPostgresDAO.getUnspentOutputs(address) Good(result) } + + override def getByBlockhash( + blockhash: Blockhash, + paginatedQuery: PaginatedQuery, + ordering: FieldOrdering[TransactionField]): ApplicationResult[PaginatedResult[TransactionWithValues]] = withConnection { implicit conn => + + val transactions = transactionPostgresDAO.getByBlockhash(blockhash, paginatedQuery, ordering) + val total = transactionPostgresDAO.countByBlockhash(blockhash) + val result = PaginatedResult(paginatedQuery.offset, paginatedQuery.limit, total, transactions) + + 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 3773614..93f79c6 100644 --- a/server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala +++ b/server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala @@ -145,6 +145,51 @@ class TransactionPostgresDAO @Inject() (fieldOrderingSQLInterpreter: FieldOrderi Count(result) } + def getByBlockhash( + blockhash: Blockhash, + paginatedQuery: PaginatedQuery, + ordering: FieldOrdering[TransactionField])( + implicit conn: Connection): List[TransactionWithValues] = { + + val orderBy = fieldOrderingSQLInterpreter.toOrderByClause(ordering) + + /** + * TODO: The query is very slow while aggregating the spent and received values, + * it might be worth creating an index-like table to get the accumulated + * values directly. + */ + SQL( + s""" + |SELECT t.txid, blockhash, t.time, t.size, + | (SELECT COALESCE(SUM(value), 0) FROM transaction_inputs WHERE txid = t.txid) AS sent, + | (SELECT COALESCE(SUM(value), 0) FROM transaction_outputs WHERE txid = t.txid) AS received + |FROM transactions t JOIN blocks USING (blockhash) + |WHERE blockhash = {blockhash} + |$orderBy + |OFFSET {offset} + |LIMIT {limit} + """.stripMargin + ).on( + 'blockhash -> blockhash.string, + 'offset -> paginatedQuery.offset.int, + 'limit -> paginatedQuery.limit.int + ).as(parseTransactionWithValues.*).flatten + } + + def countByBlockhash(blockhash: Blockhash)(implicit conn: Connection): Count = { + val result = SQL( + """ + |SELECT COUNT(*) + |FROM blocks JOIN transactions USING (blockhash) + |WHERE blockhash = {blockhash} + """.stripMargin + ).on( + 'blockhash -> blockhash.string + ).as(SqlParser.scalar[Int].single) + + Count(result) + } + def getUnspentOutputs(address: Address)(implicit conn: Connection): List[Transaction.Output] = { SQL( """ diff --git a/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala b/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala index 45fa68e..863e485 100644 --- a/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala +++ b/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala @@ -3,7 +3,7 @@ package com.xsn.explorer.data.async import javax.inject.Inject import com.alexitc.playsonify.core.{FutureApplicationResult, FuturePaginatedResult} -import com.alexitc.playsonify.models.{FieldOrdering, PaginatedQuery} +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._ @@ -27,4 +27,12 @@ class TransactionFutureDataHandler @Inject() ( override def getUnspentOutputs(address: Address): FutureApplicationResult[List[Transaction.Output]] = Future { blockingDataHandler.getUnspentOutputs(address) } + + override def getByBlockhash( + blockhash: Blockhash, + paginatedQuery: PaginatedQuery, + ordering: FieldOrdering[TransactionField]): FutureApplicationResult[PaginatedResult[TransactionWithValues]] = Future { + + blockingDataHandler.getByBlockhash(blockhash, paginatedQuery, ordering) + } } diff --git a/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala b/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala index e0230ba..91fecf6 100644 --- a/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala +++ b/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala @@ -196,6 +196,106 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be } } + "getByBlockhash" should { + val blockhash = createBlockhash("0000000000bdbb23e28f79a49d29b41429737c6c7e15df40d1b1f1b35907ae34") + val inputs = List( + Transaction.Input(dummyTransaction.id, 0, 1, 100, createAddress("XxQ7j37LfuXgsLd5DZAwFKhT3s2ZMkW86F")), + Transaction.Input(dummyTransaction.id, 1, 2, 200, createAddress("XxQ7j37LfuXgsLD5DZAwFKhT3s2ZMkW86F")) + ) + + val outputs = List( + Transaction.Output(createTransactionId("ad9320dcea2fdaa357aac6eab00695cf07b487e34113598909f625c24629c981"), 0, BigDecimal(50), createAddress("Xbh5pJdBNm8J9PxnEmwVcuQKRmZZ7DkpcF"), HexString.from("00").get, None, None), + Transaction.Output( + createTransactionId("ad9320dcea2fdaa357aac6eab00695cf07b487e34113598909f625c24629c981"), + 1, + BigDecimal(250), + createAddress("Xbh5pJdBNm8J9PxnEmwVcuQKRmZZ7DkpcF"), + HexString.from("00").get, + None, None) + ) + + val transactions = List( + Transaction( + createTransactionId("00051e4fe89466faa734d6207a7ef6115fa1dd33f7156b006fafc6bb85a79eb8"), + blockhash, + 12312312L, + Size(1000), + inputs, + outputs), + Transaction( + createTransactionId("02c51e4fe89466faa734d6207a7ef6115fa1dd33f7156b006fafc6bb85a79eb8"), + blockhash, + 12312302L, + Size(900), + inputs.map(x => x.copy(value = x.value * 2)), + outputs.map(x => x.copy(value = x.value * 2))), + Transaction( + createTransactionId("00c51e4fe89466faa734d6207a7ef6115fa1dd33f7156b006fafc6bb85a79eb8"), + blockhash, + 12312310L, + Size(100), + inputs.map(x => x.copy(value = x.value / 2)), + outputs.map(x => x.copy(value = x.value / 2))) + ) + + val block = this.block.copy( + hash = blockhash, + height = Height(10), + transactions = transactions.map(_.id)) + + "find no results" in { + val blockhash = createBlockhash("021d335a910f6780bdf48f9efd751b162074367eeb6740ac205223496430260f") + val query = PaginatedQuery(Offset(0), Limit(10)) + val expected = PaginatedResult(query.offset, query.limit, Count(0), List.empty) + val result = dataHandler.getByBlockhash(blockhash, query, defaultOrdering) + + result mustEqual Good(expected) + } + + "find the right values" in { + createBlock(block, transactions) + + val query = PaginatedQuery(Offset(0), Limit(10)) + val result = dataHandler.getByBlockhash(blockhash, query, defaultOrdering).get + + result.total mustEqual Count(transactions.size) + result.offset mustEqual query.offset + result.limit mustEqual query.limit + result.data.size mustEqual transactions.size + } + + def testOrdering[B](field: TransactionField)(sortBy: Transaction => B)(implicit order: Ordering[B]) = { + createBlock(block, transactions) + + val ordering = FieldOrdering(field, OrderingCondition.AscendingOrder) + val query = PaginatedQuery(Offset(0), Limit(10)) + + val expected = transactions.sortBy(sortBy)(order).map(_.id) + val result = dataHandler.getByBlockhash(blockhash, query, ordering).get.data + result.map(_.id) mustEqual expected + + val expectedReverse = expected.reverse + val resultReverse = dataHandler.getByBlockhash(blockhash, query, ordering.copy(orderingCondition = OrderingCondition.DescendingOrder)).get.data + resultReverse.map(_.id) mustEqual expectedReverse + } + + "allow to sort by txid" in { + testOrdering(TransactionField.TransactionId)(_.id.string) + } + + "allow to sort by time" in { + testOrdering(TransactionField.Time)(_.time) + } + + "allow to sort by sent" in { + testOrdering(TransactionField.Sent)(_.inputs.map(_.value).sum) + } + + "allow to sort by received" in { + testOrdering(TransactionField.Received)(_.outputs.map(_.value).sum) + } + } + private def createBlock(block: Block) = { val transactions = block.transactions .map(_.string) @@ -206,4 +306,10 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be result.isGood mustEqual true } + + private def createBlock(block: Block, transactions: List[Transaction]) = { + val result = ledgerDataHandler.push(block, transactions) + + result.isGood mustEqual true + } } diff --git a/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala b/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala index 37a3d30..df15090 100644 --- a/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala +++ b/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala @@ -11,4 +11,6 @@ class TransactionDummyDataHandler extends TransactionBlockingDataHandler { override def getBy(address: Address, paginatedQuery: PaginatedQuery, ordering: FieldOrdering[TransactionField]): ApplicationResult[PaginatedResult[TransactionWithValues]] = ??? override def getUnspentOutputs(address: Address): ApplicationResult[List[Transaction.Output]] = ??? + + override def getByBlockhash(blockhash: Blockhash, paginatedQuery: PaginatedQuery, ordering: FieldOrdering[TransactionField]): ApplicationResult[PaginatedResult[TransactionWithValues]] = ??? }