Skip to content

Commit

Permalink
server: Update endpoint - GET /addresses/:address/lightWalletTransact…
Browse files Browse the repository at this point in the history
…ions

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.
  • Loading branch information
AlexITC committed Dec 28, 2018
1 parent 4feee27 commit a03e440
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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]]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 11 additions & 4 deletions server/app/com/xsn/explorer/services/TransactionService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
6 changes: 3 additions & 3 deletions server/app/controllers/AddressesController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion server/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]] = ???

Expand Down

0 comments on commit a03e440

Please sign in to comment.