diff --git a/server/app/com/xsn/explorer/data/TransactionDataHandler.scala b/server/app/com/xsn/explorer/data/TransactionDataHandler.scala index 9ecfc97..b9033d1 100644 --- a/server/app/com/xsn/explorer/data/TransactionDataHandler.scala +++ b/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]] diff --git a/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala b/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala index cab58e7..382cd34 100644 --- a/server/app/com/xsn/explorer/data/anorm/TransactionPostgresDataHandler.scala +++ b/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) } 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 f6b66e8..26e1ae2 100644 --- a/server/app/com/xsn/explorer/data/anorm/dao/TransactionPostgresDAO.scala +++ b/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, diff --git a/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala b/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala index 81bc2e3..ccc60d0 100644 --- a/server/app/com/xsn/explorer/data/async/TransactionFutureDataHandler.scala +++ b/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 { diff --git a/server/app/com/xsn/explorer/services/TransactionService.scala b/server/app/com/xsn/explorer/services/TransactionService.scala index b31f589..7e9aba1 100644 --- a/server/app/com/xsn/explorer/services/TransactionService.scala +++ b/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 diff --git a/server/app/controllers/AddressesController.scala b/server/app/controllers/AddressesController.scala index 70cf8bb..43e9ecc 100644 --- a/server/app/controllers/AddressesController.scala +++ b/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) } /** diff --git a/server/conf/routes b/server/conf/routes index 54d6c8f..6e04729 100644 --- a/server/conf/routes +++ b/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() diff --git a/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala b/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala index 9e4fdd0..67dbf9e 100644 --- a/server/test/com/xsn/explorer/data/TransactionPostgresDataHandlerSpec.scala +++ b/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)) } } diff --git a/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala b/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala index c95a6b4..2e41f2c 100644 --- a/server/test/com/xsn/explorer/helpers/TransactionDummyDataHandler.scala +++ b/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]] = ???