diff --git a/server/app/com/xsn/explorer/data/TransactionDataHandler.scala b/server/app/com/xsn/explorer/data/TransactionDataHandler.scala index e9393d8..b32a5ff 100644 --- a/server/app/com/xsn/explorer/data/TransactionDataHandler.scala +++ b/server/app/com/xsn/explorer/data/TransactionDataHandler.scala @@ -1,7 +1,8 @@ package com.xsn.explorer.data import com.alexitc.playsonify.core.ApplicationResult -import com.xsn.explorer.models.{Blockhash, Transaction, TransactionId} +import com.alexitc.playsonify.models.{PaginatedQuery, PaginatedResult} +import com.xsn.explorer.models._ import scala.language.higherKinds @@ -12,6 +13,8 @@ trait TransactionDataHandler[F[_]] { def delete(transactionId: TransactionId): F[Transaction] def deleteBy(blockhash: Blockhash): F[List[Transaction]] + + def getBy(address: Address, paginatedQuery: PaginatedQuery): 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 ccb17fa..7532863 100644 --- a/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala +++ b/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala @@ -3,10 +3,11 @@ package com.xsn.explorer.data.anorm import javax.inject.Inject import com.alexitc.playsonify.core.ApplicationResult +import com.alexitc.playsonify.models.{PaginatedQuery, PaginatedResult} import com.xsn.explorer.data.TransactionBlockingDataHandler import com.xsn.explorer.data.anorm.dao.TransactionPostgresDAO import com.xsn.explorer.errors.{TransactionNotFoundError, TransactionUnknownError} -import com.xsn.explorer.models.{Blockhash, Transaction, TransactionId} +import com.xsn.explorer.models._ import org.scalactic.{Good, One, Or} import play.api.db.Database @@ -30,4 +31,15 @@ class TransactionPostgresDataHandler @Inject() ( val transactions = transactionPostgresDAO.deleteBy(blockhash) Good(transactions) } + + override def getBy( + address: Address, + paginatedQuery: PaginatedQuery): ApplicationResult[PaginatedResult[TransactionWithValues]] = withConnection { implicit conn => + + val transactions = transactionPostgresDAO.getBy(address, paginatedQuery) + val total = transactionPostgresDAO.countBy(address) + 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 dd17bbd..7ca6798 100644 --- a/server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala +++ b/server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala @@ -3,8 +3,9 @@ package com.xsn.explorer.data.anorm.dao import java.sql.Connection import anorm._ +import com.alexitc.playsonify.models.{Count, PaginatedQuery} import com.xsn.explorer.data.anorm.parsers.TransactionParsers._ -import com.xsn.explorer.models.{Blockhash, Transaction, TransactionId} +import com.xsn.explorer.models._ class TransactionPostgresDAO { @@ -63,6 +64,60 @@ class TransactionPostgresDAO { } } + def getBy(address: Address, paginatedQuery: PaginatedQuery)(implicit conn: Connection): List[TransactionWithValues] = { + /** + * 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( + """ + |SELECT t.txid, blockhash, time, 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 + |WHERE t.txid IN ( + | SELECT txid + | FROM transaction_inputs + | WHERE address = {address} + |) OR t.txid IN ( + | SELECT txid + | FROM transaction_outputs + | WHERE address = {address} + |) + |ORDER BY time DESC + |OFFSET {offset} + |LIMIT {limit} + """.stripMargin + ).on( + 'address -> address.string, + 'offset -> paginatedQuery.offset.int, + 'limit -> paginatedQuery.limit.int + ).as(parseTransactionWithValues.*).flatten + } + + def countBy(address: Address)(implicit conn: Connection): Count = { + val result = SQL( + """ + |SELECT COUNT(*) + |FROM transactions + |WHERE txid IN ( + | SELECT txid + | FROM transaction_inputs + | WHERE address = {address} + |) OR txid IN ( + | SELECT txid + | FROM transaction_outputs + | WHERE address = {address} + |) + """.stripMargin + ).on( + 'address -> address.string + ).as(SqlParser.scalar[Int].single) + + Count(result) + } + private def upsertTransaction(transaction: Transaction)(implicit conn: Connection): Option[Transaction] = { SQL( """ diff --git a/server/app/com/xsn/explorer/data/anorm/parsers/TransactionParsers.scala b/server/app/com/xsn/explorer/data/anorm/parsers/TransactionParsers.scala index 88c375f..8622c04 100644 --- a/server/app/com/xsn/explorer/data/anorm/parsers/TransactionParsers.scala +++ b/server/app/com/xsn/explorer/data/anorm/parsers/TransactionParsers.scala @@ -2,7 +2,7 @@ package com.xsn.explorer.data.anorm.parsers import anorm.SqlParser.{get, str} import anorm.~ -import com.xsn.explorer.models.{Address, Transaction, TransactionId} +import com.xsn.explorer.models.{Address, Transaction, TransactionId, TransactionWithValues} object TransactionParsers { @@ -11,6 +11,7 @@ object TransactionParsers { val parseTransactionId = str("txid").map(TransactionId.from) val parseReceived = get[BigDecimal]("received") val parseSpent = get[BigDecimal]("spent") + val parseSent = get[BigDecimal]("sent") val parseIndex = get[Int]("index") val parseValue = get[BigDecimal]("value") @@ -26,6 +27,21 @@ object TransactionParsers { } yield Transaction(txid, blockhash, time, size, List.empty, List.empty) } + val parseTransactionWithValues = ( + parseTransactionId ~ + parseBlockhash ~ + parseTime ~ + parseSize ~ + parseSent ~ + parseReceived).map { + + case txidMaybe ~ blockhashMaybe ~ time ~ size ~ sent ~ received => + for { + txid <- txidMaybe + blockhash <- blockhashMaybe + } yield TransactionWithValues(txid, blockhash, time, size, sent, received) + } + val parseTransactionInput = (parseIndex ~ parseValue.? ~ parseAddress.?).map { case index ~ value ~ address => Transaction.Input(index, value, address.flatten) } diff --git a/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala b/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala index c3e9166..3aca053 100644 --- a/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala +++ b/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala @@ -2,10 +2,11 @@ package com.xsn.explorer.data.async import javax.inject.Inject -import com.alexitc.playsonify.core.FutureApplicationResult +import com.alexitc.playsonify.core.{FutureApplicationResult, FuturePaginatedResult} +import com.alexitc.playsonify.models.PaginatedQuery import com.xsn.explorer.data.{TransactionBlockingDataHandler, TransactionDataHandler} import com.xsn.explorer.executors.DatabaseExecutionContext -import com.xsn.explorer.models.{Blockhash, Transaction, TransactionId} +import com.xsn.explorer.models._ import scala.concurrent.Future @@ -25,4 +26,11 @@ class TransactionFutureDataHandler @Inject() ( override def deleteBy(blockhash: Blockhash): FutureApplicationResult[List[Transaction]] = Future { blockingDataHandler.deleteBy(blockhash) } + + override def getBy( + address: Address, + paginatedQuery: PaginatedQuery): FuturePaginatedResult[TransactionWithValues] = Future { + + blockingDataHandler.getBy(address, paginatedQuery) + } } diff --git a/server/app/com/xsn/explorer/models/TransactionWithValues.scala b/server/app/com/xsn/explorer/models/TransactionWithValues.scala new file mode 100644 index 0000000..a89430a --- /dev/null +++ b/server/app/com/xsn/explorer/models/TransactionWithValues.scala @@ -0,0 +1,16 @@ +package com.xsn.explorer.models + +import play.api.libs.json.{Json, Writes} + +case class TransactionWithValues( + id: TransactionId, + blockhash: Blockhash, + time: Long, + size: Size, + sent: BigDecimal, + received: BigDecimal) + +object TransactionWithValues { + + implicit val writes: Writes[TransactionWithValues] = Json.writes[TransactionWithValues] +}