Skip to content

Commit b547fbe

Browse files
committed
server: added new api endpoint for paginated transaction list
1 parent 3b74262 commit b547fbe

13 files changed

Lines changed: 268 additions & 2 deletions

server/app/com/xsn/explorer/data/TransactionDataHandler.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ trait TransactionDataHandler[F[_]] {
2222

2323
def getOutput(txid: TransactionId, index: Int): F[Transaction.Output]
2424

25+
def get(
26+
limit: Limit,
27+
lastSeenTxid: Option[TransactionId],
28+
orderingCondition: OrderingCondition
29+
): F[List[TransactionInfo]]
30+
2531
def getByBlockhash(
2632
blockhash: Blockhash,
2733
limit: Limit,

server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ class TransactionPostgresDataHandler @Inject()(
4545
Or.from(maybe, One(TransactionError.OutputNotFound(txid, index)))
4646
}
4747

48+
override def get(
49+
limit: Limit,
50+
lastSeenTxid: Option[TransactionId],
51+
orderingCondition: OrderingCondition
52+
): ApplicationResult[List[TransactionInfo]] = withConnection { implicit conn =>
53+
val transactions = lastSeenTxid
54+
.map { transactionPostgresDAO.get(_, limit, orderingCondition) }
55+
.getOrElse { transactionPostgresDAO.get(limit, orderingCondition) }
56+
57+
Good(transactions)
58+
}
59+
4860
override def getByBlockhash(
4961
blockhash: Blockhash,
5062
limit: Limit,

server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,57 @@ class TransactionPostgresDAO @Inject()(
311311
.as(parseTransactionWithValues.*)
312312
}
313313

314+
def get(limit: Limit, orderingCondition: OrderingCondition)(implicit conn: Connection): List[TransactionInfo] = {
315+
316+
val order = toSQL(orderingCondition)
317+
318+
SQL(
319+
s"""
320+
|SELECT t.txid, t.blockhash, t.time, t.size, t.height,
321+
| (SELECT COALESCE(SUM(value), 0) FROM transaction_inputs WHERE txid = t.txid) AS sent,
322+
| (SELECT COALESCE(SUM(value), 0) FROM transaction_outputs WHERE txid = t.txid) AS received
323+
|FROM transactions t JOIN blocks USING (blockhash)
324+
|ORDER BY t.height $order
325+
|LIMIT {limit}
326+
""".stripMargin
327+
).on(
328+
'limit -> limit.int
329+
)
330+
.as(parseTransactionInfo.*)
331+
}
332+
333+
def get(lastSeenTxid: TransactionId, limit: Limit, orderingCondition: OrderingCondition)(
334+
implicit conn: Connection
335+
): List[TransactionInfo] = {
336+
337+
val order = toSQL(orderingCondition)
338+
val comparator = orderingCondition match {
339+
case OrderingCondition.DescendingOrder => "<"
340+
case OrderingCondition.AscendingOrder => ">"
341+
}
342+
343+
SQL(
344+
s"""
345+
|WITH CTE AS (
346+
| SELECT height AS lastSeenHeight
347+
| FROM transactions
348+
| WHERE txid = {lastSeenTxid}
349+
|)
350+
|SELECT t.txid, t.blockhash, t.time, t.size, t.height,
351+
| (SELECT COALESCE(SUM(value), 0) FROM transaction_inputs WHERE txid = t.txid) AS sent,
352+
| (SELECT COALESCE(SUM(value), 0) FROM transaction_outputs WHERE txid = t.txid) AS received
353+
|FROM CTE CROSS JOIN transactions t JOIN blocks USING (blockhash)
354+
|WHERE t.height $comparator lastSeenHeight
355+
|ORDER BY t.height $order
356+
|LIMIT {limit}
357+
""".stripMargin
358+
).on(
359+
'limit -> limit.int,
360+
'lastSeenTxid -> lastSeenTxid.toBytesBE.toArray
361+
)
362+
.as(parseTransactionInfo.*)
363+
}
364+
314365
def getTransactionsWithIOBy(blockhash: Blockhash, limit: Limit)(
315366
implicit conn: Connection
316367
): List[Transaction.HasIO] = {

server/app/com/xsn/explorer/data/anorm/parsers/TransactionParsers.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import anorm.SqlParser._
44
import anorm.~
55
import com.xsn.explorer.models._
66
import com.xsn.explorer.models.persisted.{AddressTransactionDetails, Transaction}
7+
import com.xsn.explorer.models.values._
78

89
object TransactionParsers {
910

@@ -16,6 +17,7 @@ object TransactionParsers {
1617
val parseSent = get[BigDecimal]("sent")
1718
val parseValue = get[BigDecimal]("value")
1819
val parseHexScript = parseHexString("hex_script")
20+
val parseHeight = int("height").map(Height.apply)
1921

2022
val parseTransaction = (parseTransactionId() ~ parseBlockhashBytes() ~ parseTime ~ parseSize).map {
2123
case txid ~ blockhash ~ time ~ size => Transaction(txid, blockhash, time, size)
@@ -32,6 +34,18 @@ object TransactionParsers {
3234
TransactionWithValues(txid, blockhash, time, size, sent, received)
3335
}
3436

37+
val parseTransactionInfo = (parseTransactionId() ~
38+
parseBlockhashBytes() ~
39+
parseTime ~
40+
parseSize ~
41+
parseSent ~
42+
parseReceived ~
43+
parseHeight).map {
44+
45+
case txid ~ blockhash ~ time ~ size ~ sent ~ received ~ height =>
46+
TransactionInfo(txid, blockhash, time, size, sent, received, height)
47+
}
48+
3549
val parseTransactionInput =
3650
(parseFromTxid ~ parseFromOutputIndex ~ parseIndex ~ parseValue ~ parseAddresses)
3751
.map {

server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ class TransactionFutureDataHandler @Inject()(
4545
}
4646
}
4747

48+
override def get(
49+
limit: Limit,
50+
lastSeenTxid: Option[TransactionId],
51+
orderingCondition: OrderingCondition
52+
): FutureApplicationResult[List[TransactionInfo]] = retryableFutureDataHandler.retrying {
53+
Future {
54+
blockingDataHandler.get(limit, lastSeenTxid, orderingCondition)
55+
}
56+
}
57+
4858
override def getByBlockhash(
4959
blockhash: Blockhash,
5060
limit: Limit,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.xsn.explorer.models
2+
3+
import com.xsn.explorer.models.values.{Blockhash, Height, Size, TransactionId}
4+
import play.api.libs.json.{Json, Writes}
5+
6+
case class TransactionInfo(
7+
id: TransactionId,
8+
blockhash: Blockhash,
9+
time: Long,
10+
size: Size,
11+
sent: BigDecimal,
12+
received: BigDecimal,
13+
height: Height
14+
)
15+
16+
object TransactionInfo {
17+
18+
implicit val writes: Writes[TransactionInfo] = Json.writes[TransactionInfo]
19+
}

server/app/com/xsn/explorer/services/TransactionService.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ class TransactionService @Inject()(
4848
result.toFuture
4949
}
5050

51+
def get(
52+
limit: Limit,
53+
lastSeenTxidString: Option[String],
54+
orderingConditionString: String
55+
): FutureApplicationResult[WrappedResult[List[TransactionInfo]]] = {
56+
val result = for {
57+
_ <- paginatedQueryValidator.validate(PaginatedQuery(Offset(0), limit), maxTransactionsPerQuery).toFutureOr
58+
59+
lastSeenTxid <- validate(lastSeenTxidString, transactionIdValidator.validate).toFutureOr
60+
orderingCondition <- orderingConditionParser.parseReuslt(orderingConditionString).toFutureOr
61+
62+
r <- transactionFutureDataHandler.get(limit, lastSeenTxid, orderingCondition).toFutureOr
63+
} yield WrappedResult(r)
64+
65+
result.toFuture
66+
}
67+
5168
def getByBlockhash(
5269
blockhashString: String,
5370
limit: Limit,

server/app/controllers/TransactionsController.scala

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,28 @@ package controllers
33
import com.alexitc.playsonify.core.FutureOr.Implicits.FutureOps
44
import com.xsn.explorer.models.request.SendRawTransactionRequest
55
import com.xsn.explorer.services.TransactionRPCService
6+
import com.xsn.explorer.services.TransactionService
7+
import com.alexitc.playsonify.models.pagination.Limit
68
import controllers.common.{MyJsonController, MyJsonControllerComponents}
79
import javax.inject.Inject
810
import play.api.libs.json.Json
911

10-
class TransactionsController @Inject()(transactionRPCService: TransactionRPCService, cc: MyJsonControllerComponents)
12+
class TransactionsController @Inject()(transactionRPCService: TransactionRPCService, transactionService: TransactionService, cc: MyJsonControllerComponents)
1113
extends MyJsonController(cc) {
1214

1315
import Context._
1416

17+
def getTransactions(limit: Int, lastSeenTxid: Option[String], orderingCondition: String) = public { _ =>
18+
transactionService
19+
.get(Limit(limit), lastSeenTxid, orderingCondition)
20+
.toFutureOr
21+
.map { value =>
22+
val response = Ok(Json.toJson(value))
23+
response.withHeaders("Cache-Control" -> "public, max-age=60")
24+
}
25+
.toFuture
26+
}
27+
1528
def getTransaction(txid: String) = public { _ =>
1629
transactionRPCService
1730
.getTransactionDetails(txid)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
# --- !Ups
3+
ALTER TABLE transactions
4+
ADD COLUMN height SERIAL;
5+
6+
# --- !Downs
7+
ALTER TABLE transactions
8+
DROP COLUMN height;

server/conf/routes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
GET /health controllers.HealthController.check()
77
# GET /maintenance controllers.MaintenanceController.run(query: String ?= "")
88

9+
GET /transactions controllers.TransactionsController.getTransactions(limit: Int ?= 10, lastSeenTxid: Option[String], order: String ?= "desc")
910
GET /transactions/:txid controllers.TransactionsController.getTransaction(txid: String)
1011
GET /transactions/:txid/raw controllers.TransactionsController.getRawTransaction(txid: String)
1112
POST /transactions controllers.TransactionsController.sendRawTransaction()

0 commit comments

Comments
 (0)