Browse Source

server: Update endpoint - GET /addresses/:address/lightWalletTransactions

The pagination is updated to check the latest seen transaction instead of its time,
this avoids hiding items when there are ties on the time.
prometheus-integration
Alexis Hernandez 6 years ago
parent
commit
a03e4401ca
  1. 2
      server/app/com/xsn/explorer/data/TransactionDataHandler.scala
  2. 7
      server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala
  3. 53
      server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala
  4. 7
      server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala
  5. 15
      server/app/com/xsn/explorer/services/TransactionService.scala
  6. 6
      server/app/controllers/AddressesController.scala
  7. 2
      server/conf/routes
  8. 80
      server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala
  9. 2
      server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala

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

@ -16,7 +16,7 @@ trait TransactionDataHandler[F[_]] {
paginatedQuery: PaginatedQuery,
ordering: FieldOrdering[TransactionField]): F[PaginatedResult[TransactionWithValues]]
def getBy(address: Address, before: Long, limit: Limit): F[List[Transaction]]
def getLatestBy(address: Address, limit: Limit, lastSeenTxid: Option[TransactionId]): F[List[Transaction]]
def getUnspentOutputs(address: Address): F[List[Transaction.Output]]

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

@ -29,8 +29,11 @@ class TransactionPostgresDataHandler @Inject() (
Good(result)
}
def getBy(address: Address, before: Long, limit: Limit): ApplicationResult[List[Transaction]] = withConnection { implicit conn =>
val transactions = transactionPostgresDAO.getBy(address, before, limit)
def getLatestBy(address: Address, limit: Limit, lastSeenTxid: Option[TransactionId]): ApplicationResult[List[Transaction]] = withConnection { implicit conn =>
val transactions = lastSeenTxid
.map { transactionPostgresDAO.getLatestBy(address, _, limit) }
.getOrElse { transactionPostgresDAO.getLatestBy(address, limit) }
Good(transactions)
}

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

@ -88,22 +88,67 @@ class TransactionPostgresDAO @Inject() (fieldOrderingSQLInterpreter: FieldOrderi
.getOrElse { throw new RuntimeException("Failed to delete transactions consistently")} // this should not happen
}
def getBy(address: Address, before: Long, limit: Limit)(implicit conn: Connection): List[Transaction] = {
/**
* Get the latest transactions by the given address.
*/
def getLatestBy(address: Address, limit: Limit)(implicit conn: Connection): List[Transaction] = {
SQL(
"""
|SELECT t.txid, t.blockhash, t.time, t.size
|FROM transactions t JOIN address_transaction_details USING (txid)
|WHERE t.time < {before} AND address = {address}
|WHERE address = {address}
|ORDER BY time DESC
|LIMIT {limit}
""".stripMargin
).on(
'address -> address.string,
'limit -> limit.int,
'before -> before
'limit -> limit.int
).as(parseTransaction.*).flatten
}
/**
* Get the latest transactions by the given address that occurred before the last seen transaction.
*/
def getLatestBy(
address: Address,
lastSeenTxid: TransactionId,
limit: Limit)(
implicit conn: Connection): List[Transaction] = {
/**
* TODO: Update query to:
WITH CTE AS (
SELECT time AS lastSeenTime
FROM transactions
WHERE txid = {lastSeenTxid}
)
SELECT t.txid, t.blockhash, t.time, t.size
FROM CTE CROSS JOIN transactions t
JOIN address_transaction_details USING (txid)
WHERE address = {address} AND
(t.time < lastSeenTime OR (t.time = lastSeenTime AND t.txid > {lastSeenTxid}))
ORDER BY time DESC
LIMIT {limit}
*/
SQL(
"""
|SELECT t.txid, t.blockhash, t.time, t.size
|FROM transactions t
| JOIN address_transaction_details USING (txid)
|WHERE address = {address} AND
| (t.time < (SELECT time AS lastSeenTime FROM transactions WHERE txid = {lastSeenTxid}) OR
| (t.time = (SELECT time AS lastSeenTime FROM transactions WHERE txid = {lastSeenTxid}) AND
| t.txid > {lastSeenTxid}))
|ORDER BY time DESC
|LIMIT {limit}
""".stripMargin
).on(
'address -> address.string,
'limit -> limit.int,
'lastSeenTxid -> lastSeenTxid.string
).executeQuery().as(parseTransaction.*).flatten
}
def getBy(
address: Address,
paginatedQuery: PaginatedQuery,

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

@ -25,12 +25,9 @@ class TransactionFutureDataHandler @Inject() (
blockingDataHandler.getBy(address, paginatedQuery, ordering)
}
override def getBy(
address: Address,
before: Long,
limit: Limit): FutureApplicationResult[List[Transaction]] = Future {
override def getLatestBy(address: Address, limit: Limit, lastSeenTxid: Option[TransactionId]): FutureApplicationResult[List[Transaction]] = Future {
blockingDataHandler.getBy(address, before, limit)
blockingDataHandler.getLatestBy(address, limit, lastSeenTxid)
}
override def getUnspentOutputs(address: Address): FutureApplicationResult[List[Transaction.Output]] = Future {

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

@ -150,7 +150,7 @@ class TransactionService @Inject() (
result.toFuture
}
def getLightWalletTransactions(addressString: String, before: Long, limit: Limit): FutureApplicationResult[List[LightWalletTransaction]] = {
def getLightWalletTransactions(addressString: String, limit: Limit, lastSeenTxidString: Option[String]): FutureApplicationResult[List[LightWalletTransaction]] = {
def buildData(address: Address, txValues: Transaction) = {
val result = for {
plain <- xsnService.getTransaction(txValues.id).toFutureOr
@ -175,16 +175,23 @@ class TransactionService @Inject() (
result.toFuture
}
val paginatedQuery = PaginatedQuery(Offset(0), limit)
val result = for {
address <- {
val maybe = Address.from(addressString)
Or.from(maybe, One(AddressFormatError)).toFutureOr
}
_ <- paginatedQueryValidator.validate(paginatedQuery, maxTransactionsPerQuery).toFutureOr
_ <- paginatedQueryValidator.validate(PaginatedQuery(Offset(0), limit), maxTransactionsPerQuery).toFutureOr
transactions <- transactionFutureDataHandler.getBy(address, before, limit).toFutureOr
lastSeenTxid <- {
lastSeenTxidString
.map(TransactionId.from)
.map { txid => Or.from(txid, One(TransactionFormatError)).map(Option.apply) }
.getOrElse(Good(Option.empty))
.toFutureOr
}
transactions <- transactionFutureDataHandler.getLatestBy(address, limit, lastSeenTxid).toFutureOr
data <- transactions.map { transaction => buildData(address, transaction) }.toFutureOr
} yield data

6
server/app/controllers/AddressesController.scala

@ -27,11 +27,11 @@ class AddressesController @Inject() (
transactionService.getTransactions(address, paginatedQuery, OrderingQuery(ordering))
}
def getLightWalletTransactions(address: String, limit: Int, before: Option[Long]) = public { _ =>
def getLightWalletTransactions(address: String, limit: Int, lastSeenTxid: Option[String]) = public { _ =>
transactionService.getLightWalletTransactions(
address,
before.getOrElse(java.lang.System.currentTimeMillis()),
Limit(limit))
Limit(limit),
lastSeenTxid)
}
/**

2
server/conf/routes

@ -12,7 +12,7 @@ POST /transactions/latest controllers.TransactionsController.getLatestByA
GET /addresses/:address controllers.AddressesController.getBy(address: String)
GET /addresses/:address/transactions controllers.AddressesController.getTransactions(address: String, offset: Int ?= 0, limit: Int ?= 10, orderBy: String ?= "")
GET /addresses/:address/lightWalletTransactions controllers.AddressesController.getLightWalletTransactions(address: String, limit: Int ?= 10, before: Option[Long])
GET /addresses/:address/lightWalletTransactions controllers.AddressesController.getLightWalletTransactions(address: String, limit: Int ?= 10, lastSeenTxid: Option[String])
GET /addresses/:address/utxos controllers.AddressesController.getUnspentOutputs(address: String)
GET /blocks controllers.BlocksController.getLatestBlocks()

80
server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala

@ -319,9 +319,83 @@ class TransactionPostgresDataHandlerSpec extends PostgresDataHandlerSpec with Be
}
}
"getBy keyset pagination" should {
"work" in {
pending
"getLatestBy" should {
val address = createAddress("XxQ7j37LfuXgsLD5DZAwFKhT3s2ZMkW86F")
val blockhash = createBlockhash("0000000000bdbb23e28f79a49d29b41429737c6c7e15df40d1b1f1b35907ae34")
val inputs = List(
Transaction.Input(dummyTransaction.id, 0, 1, 100, address),
Transaction.Input(dummyTransaction.id, 1, 2, 200, address)
)
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 transaction = Transaction(
createTransactionId("00051e4fe89466faa734d6207a7ef6115fa1dd33f7156b006fafc6bb85a79eb8"),
blockhash,
321,
Size(1000),
inputs,
outputs)
val transactions = List(
transaction,
transaction.copy(
id = createTransactionId("00041e4fe89466faa734d6207a7ef6115fa1dd33f7156b006fafc6bb85a79eb8"),
time = 320),
transaction.copy(
id = createTransactionId("00c51e4fe89466faa734d6207a7ef6115fa1dd33f7156b006fafc6bb85a79eb8"),
time = 319),
transaction.copy(
id = createTransactionId("02c51e4fe89466faa734d6207a7ef6115fa1dd33f7156b006fafc6bb85a79eb8"),
time = 319))
.sortWith { case (a, b) =>
if (a.time > b.time) true
else if (a.time < b.time) false
else if (a.id.string < b.id.string) true
else false
}
val block = this.block.copy(
hash = blockhash,
height = Height(10),
transactions = transactions.map(_.id))
def prepare() = {
createBlock(block, transactions)
}
"return the newest elements" in {
prepare()
val expected = transactions.head
val result = dataHandler.getLatestBy(address, Limit(1), None).get
result mustEqual List(expected.copy(inputs = List.empty, outputs = List.empty))
}
"return the next elements given the last seen tx" in {
prepare()
val lastSeenTxid = transactions.head.id
val expected = transactions(1)
val result = dataHandler.getLatestBy(address, Limit(1), Option(lastSeenTxid)).get
result mustEqual List(expected.copy(inputs = List.empty, outputs = List.empty))
}
"return the element with the same time breaking ties by txid" in {
prepare()
val lastSeenTxid = transactions(2).id
val expected = transactions(3)
val result = dataHandler.getLatestBy(address, Limit(1), Option(lastSeenTxid)).get
result mustEqual List(expected.copy(inputs = List.empty, outputs = List.empty))
}
}

2
server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala

@ -13,7 +13,7 @@ class TransactionDummyDataHandler extends TransactionBlockingDataHandler {
override def getBy(address: Address, paginatedQuery: PaginatedQuery, ordering: FieldOrdering[TransactionField]): ApplicationResult[PaginatedResult[TransactionWithValues]] = ???
override def getBy(address: Address, before: Long, limit: pagination.Limit): ApplicationResult[List[Transaction]] = ???
override def getLatestBy(address: Address, limit: pagination.Limit, lastSeenTxid: Option[TransactionId]): ApplicationResult[List[Transaction]] = ???
override def getUnspentOutputs(address: Address): ApplicationResult[List[Transaction.Output]] = ???

Loading…
Cancel
Save