diff --git a/server/app/com/xsn/explorer/data/BalanceDataHandler.scala b/server/app/com/xsn/explorer/data/BalanceDataHandler.scala index edd3982..d1e78a6 100644 --- a/server/app/com/xsn/explorer/data/BalanceDataHandler.scala +++ b/server/app/com/xsn/explorer/data/BalanceDataHandler.scala @@ -2,7 +2,7 @@ package com.xsn.explorer.data import com.alexitc.playsonify.core.ApplicationResult import com.alexitc.playsonify.models.ordering.FieldOrdering -import com.alexitc.playsonify.models.pagination.{PaginatedQuery, PaginatedResult} +import com.alexitc.playsonify.models.pagination.{Limit, PaginatedQuery, PaginatedResult} import com.xsn.explorer.models.fields.BalanceField import com.xsn.explorer.models.{Address, Balance} @@ -17,6 +17,8 @@ trait BalanceDataHandler[F[_]] { def getBy(address: Address): F[Balance] def getNonZeroBalances(query: PaginatedQuery, ordering: FieldOrdering[BalanceField]): F[PaginatedResult[Balance]] + + def getHighestBalances(limit: Limit, lastSeenAddress: Option[Address]): F[List[Balance]] } trait BalanceBlockingDataHandler extends BalanceDataHandler[ApplicationResult] diff --git a/server/app/com/xsn/explorer/data/anorm/BalancePostgresDataHandler.scala b/server/app/com/xsn/explorer/data/anorm/BalancePostgresDataHandler.scala index f98a006..1ea1b45 100644 --- a/server/app/com/xsn/explorer/data/anorm/BalancePostgresDataHandler.scala +++ b/server/app/com/xsn/explorer/data/anorm/BalancePostgresDataHandler.scala @@ -2,7 +2,7 @@ package com.xsn.explorer.data.anorm import com.alexitc.playsonify.core.ApplicationResult import com.alexitc.playsonify.models.ordering.FieldOrdering -import com.alexitc.playsonify.models.pagination.{PaginatedQuery, PaginatedResult} +import com.alexitc.playsonify.models.pagination.{Limit, PaginatedQuery, PaginatedResult} import com.xsn.explorer.data.BalanceBlockingDataHandler import com.xsn.explorer.data.anorm.dao.BalancePostgresDAO import com.xsn.explorer.errors.BalanceUnknownError @@ -53,4 +53,12 @@ class BalancePostgresDataHandler @Inject() ( Good(result) } + + override def getHighestBalances(limit: Limit, lastSeenAddress: Option[Address]): ApplicationResult[List[Balance]] = withConnection { implicit conn => + val result = lastSeenAddress + .map { balancePostgresDAO.getHighestBalances(_, limit) } + .getOrElse { balancePostgresDAO.getHighestBalances(limit) } + + Good(result) + } } diff --git a/server/app/com/xsn/explorer/data/anorm/dao/BalancePostgresDAO.scala b/server/app/com/xsn/explorer/data/anorm/dao/BalancePostgresDAO.scala index 82a9adf..6eecd88 100644 --- a/server/app/com/xsn/explorer/data/anorm/dao/BalancePostgresDAO.scala +++ b/server/app/com/xsn/explorer/data/anorm/dao/BalancePostgresDAO.scala @@ -4,7 +4,7 @@ import java.sql.Connection import anorm._ import com.alexitc.playsonify.models.ordering.FieldOrdering -import com.alexitc.playsonify.models.pagination.{Count, PaginatedQuery} +import com.alexitc.playsonify.models.pagination.{Count, Limit, PaginatedQuery} import com.alexitc.playsonify.sql.FieldOrderingSQLInterpreter import com.xsn.explorer.data.anorm.parsers.BalanceParsers._ import com.xsn.explorer.models.fields.BalanceField @@ -127,4 +127,48 @@ class BalancePostgresDAO @Inject() (fieldOrderingSQLInterpreter: FieldOrderingSQ Count(result) } + + /** + * Get the highest balances (excluding hidden_addresses). + */ + def getHighestBalances(limit: Limit)(implicit conn: Connection): List[Balance] = { + SQL( + """ + |SELECT address, received, spent + |FROM balances + |WHERE address NOT IN (SELECT address FROM hidden_addresses) + |ORDER BY (received - spent) DESC + |LIMIT {limit} + """.stripMargin + ).on( + 'limit -> limit.int + ).as(parseBalance.*).flatten + } + + /** + * Get the highest balances excluding the balances until the given address (excluding hidden_addresses). + * + * Note, the results across calls might not be stable if the given address changes its balance drastically. + */ + def getHighestBalances(lastSeenAddress: Address, limit: Limit)(implicit conn: Connection): List[Balance] = { + SQL( + """ + |WITH CTE AS ( + | SELECT (received - spent) AS lastSeenAvailable + | FROM balances + | WHERE address = {lastSeenAddress} + |) + |SELECT address, received, spent + |FROM CTE CROSS JOIN balances + |WHERE ((received - spent) < lastSeenAvailable OR + | ((received - spent) = lastSeenAvailable AND address > {lastSeenAddress})) AND + | address NOT IN (SELECT address FROM hidden_addresses) + |ORDER BY (received - spent) DESC + |LIMIT {limit} + """.stripMargin + ).on( + 'limit -> limit.int, + 'lastSeenAddress -> lastSeenAddress.string + ).as(parseBalance.*).flatten + } } diff --git a/server/app/com/xsn/explorer/data/async/BalanceFutureDataHandler.scala b/server/app/com/xsn/explorer/data/async/BalanceFutureDataHandler.scala index 18fd167..689b8d7 100644 --- a/server/app/com/xsn/explorer/data/async/BalanceFutureDataHandler.scala +++ b/server/app/com/xsn/explorer/data/async/BalanceFutureDataHandler.scala @@ -2,6 +2,7 @@ package com.xsn.explorer.data.async import com.alexitc.playsonify.core.{FutureApplicationResult, FuturePaginatedResult} import com.alexitc.playsonify.models.ordering.FieldOrdering +import com.alexitc.playsonify.models.pagination import com.alexitc.playsonify.models.pagination.PaginatedQuery import com.xsn.explorer.data.{BalanceBlockingDataHandler, BalanceDataHandler} import com.xsn.explorer.executors.DatabaseExecutionContext @@ -34,4 +35,8 @@ class BalanceFutureDataHandler @Inject() ( blockingDataHandler.getNonZeroBalances(query, ordering) } + + override def getHighestBalances(limit: pagination.Limit, lastSeenAddress: Option[Address]): FutureApplicationResult[List[Balance]] = Future { + blockingDataHandler.getHighestBalances(limit, lastSeenAddress) + } } diff --git a/server/test/com/xsn/explorer/data/BalancePostgresDataHandlerSpec.scala b/server/test/com/xsn/explorer/data/BalancePostgresDataHandlerSpec.scala index f584c9f..f4a3fd4 100644 --- a/server/test/com/xsn/explorer/data/BalancePostgresDataHandlerSpec.scala +++ b/server/test/com/xsn/explorer/data/BalancePostgresDataHandlerSpec.scala @@ -13,6 +13,8 @@ import org.scalactic.Good class BalancePostgresDataHandlerSpec extends PostgresDataHandlerSpec { + import DataHelper._ + lazy val dataHandler = new BalancePostgresDataHandler(database, new BalancePostgresDAO(new FieldOrderingSQLInterpreter)) val defaultOrdering = FieldOrdering(BalanceField.Available, OrderingCondition.DescendingOrder) @@ -124,6 +126,7 @@ class BalancePostgresDataHandlerSpec extends PostgresDataHandlerSpec { .map(dataHandler.upsert) .foreach(_.isGood mustEqual true) } + "ignore addresses with balance = 0" in { prepare() val query = PaginatedQuery(Offset(0), Limit(1)) @@ -135,6 +138,76 @@ class BalancePostgresDataHandlerSpec extends PostgresDataHandlerSpec { } } + "getHighestBalances" should { + + val balances = List( + Balance(createAddress("XiHW7SR56uPHeXKwcpeVsE4nUfkHv5RqE3"), received = 1000), + Balance(createAddress("XjHW7SR56uPHeXKwcpeVsE4nUfkHv5RqE3"), received = 900), + Balance(createAddress("XkHW7SR56uPHeXKwcpeVsE4nUfkHv5RqE3"), received = 900, spent = 100), + Balance(createAddress("XlHW7SR56uPHeXKwcpeVsE4nUfkHv5RqE3"), received = 800), + Balance(createAddress("XmmmmSR56uPHeXKwcpeVsE4nUfkHv5RqE3"), received = 700), + Balance(createAddress("XnHW7SR56uPHeXKwcpeVsE4nUfkHv5RqE3"), received = 600), + Balance(createAddress("XxxxxSR56uPHeXKwcpeVsE4nUfkHv5RqE3"), received = 2000), + ) + + def prepare() = { + clearDatabase() + + balances.foreach(dataHandler.upsert(_).isGood mustEqual true) + database.withConnection { implicit conn => + _root_.anorm + .SQL( + s""" + |INSERT INTO hidden_addresses (address) VALUES + | ('${balances(4).address.string}'), + | ('${balances(6).address.string}') + |""".stripMargin) + .executeUpdate() + } + } + + "return the highest balances" in { + prepare() + + val expected = balances.head + val result = dataHandler.getHighestBalances(Limit(1), None).get + result mustEqual List(expected) + } + + "return the next elements given the last seen address" in { + prepare() + + val lastSeenAddress = balances.head.address + val expected = balances(1) + val result = dataHandler.getHighestBalances(Limit(1), Option(lastSeenAddress)).get + result mustEqual List(expected) + } + + "return the element with the same time breaking ties by address" in { + prepare() + + val lastSeenAddress = balances(2).address + val expected = balances(3) + val result = dataHandler.getHighestBalances(Limit(1), Option(lastSeenAddress)).get + result mustEqual List(expected) + } + + "return the no elements on unknown lastSeenTransaction" in { + val lastSeenAddress = createAddress("XmHW7SR56uPHeXKwcpeVsE4nUfkHv5Rq12") + val result = dataHandler.getHighestBalances(Limit(1), Option(lastSeenAddress)).get + result must be(empty) + } + + "exclude hidden_addresses" in { + prepare() + + val lastSeenAddress = balances(3).address + val expected = balances(5) + val result = dataHandler.getHighestBalances(Limit(1), Option(lastSeenAddress)).get + result mustEqual List(expected) + } + } + private def combine(balances: Balance*): Balance = { balances.reduce { (a, b) => Balance(a.address, a.received + b.received, a.spent + b.spent) diff --git a/server/test/com/xsn/explorer/data/common/PostgresDataHandlerSpec.scala b/server/test/com/xsn/explorer/data/common/PostgresDataHandlerSpec.scala index b5cfe3d..0c908bf 100644 --- a/server/test/com/xsn/explorer/data/common/PostgresDataHandlerSpec.scala +++ b/server/test/com/xsn/explorer/data/common/PostgresDataHandlerSpec.scala @@ -66,6 +66,7 @@ trait PostgresDataHandlerSpec _root_.anorm.SQL("""DELETE FROM transactions""").execute() _root_.anorm.SQL("""DELETE FROM blocks""").execute() _root_.anorm.SQL("""DELETE FROM balances""").execute() + _root_.anorm.SQL("""DELETE FROM hidden_addresses""").execute() } } diff --git a/server/test/com/xsn/explorer/helpers/BalanceDummyDataHandler.scala b/server/test/com/xsn/explorer/helpers/BalanceDummyDataHandler.scala index 866a2ac..afcfe9a 100644 --- a/server/test/com/xsn/explorer/helpers/BalanceDummyDataHandler.scala +++ b/server/test/com/xsn/explorer/helpers/BalanceDummyDataHandler.scala @@ -2,6 +2,7 @@ package com.xsn.explorer.helpers import com.alexitc.playsonify.core.ApplicationResult import com.alexitc.playsonify.models.ordering.FieldOrdering +import com.alexitc.playsonify.models.pagination import com.alexitc.playsonify.models.pagination.{PaginatedQuery, PaginatedResult} import com.xsn.explorer.data.BalanceBlockingDataHandler import com.xsn.explorer.models.fields.BalanceField @@ -16,4 +17,6 @@ class BalanceDummyDataHandler extends BalanceBlockingDataHandler { override def getBy(address: Address): ApplicationResult[Balance] = ??? override def getNonZeroBalances(query: PaginatedQuery, ordering: FieldOrdering[BalanceField]): ApplicationResult[PaginatedResult[Balance]] = ??? + + override def getHighestBalances(limit: pagination.Limit, lastSeenAddress: Option[Address]): ApplicationResult[List[Balance]] = ??? } diff --git a/server/test/controllers/BalancesControllerSpec.scala b/server/test/controllers/BalancesControllerSpec.scala index b59b675..1d9c2bd 100644 --- a/server/test/controllers/BalancesControllerSpec.scala +++ b/server/test/controllers/BalancesControllerSpec.scala @@ -4,9 +4,9 @@ import com.alexitc.playsonify.core.ApplicationResult import com.alexitc.playsonify.models.ordering.FieldOrdering import com.alexitc.playsonify.models.pagination._ import com.xsn.explorer.data.BalanceBlockingDataHandler -import com.xsn.explorer.helpers.DataHelper +import com.xsn.explorer.helpers.{BalanceDummyDataHandler, DataHelper} +import com.xsn.explorer.models.Balance import com.xsn.explorer.models.fields.BalanceField -import com.xsn.explorer.models.{Address, Balance} import controllers.common.MyAPISpec import org.scalactic.Good import play.api.inject.bind @@ -38,13 +38,7 @@ class BalancesControllerSpec extends MyAPISpec { .sortBy(_.available) .reverse - val dataHandler = new BalanceBlockingDataHandler { - - override def upsert(balance: Balance): ApplicationResult[Balance] = ??? - - override def get(query: PaginatedQuery, ordering: FieldOrdering[BalanceField]): ApplicationResult[PaginatedResult[Balance]] = ??? - - override def getBy(address: Address): ApplicationResult[Balance] = ??? + val dataHandler = new BalanceDummyDataHandler { override def getNonZeroBalances(query: PaginatedQuery, ordering: FieldOrdering[BalanceField]): ApplicationResult[PaginatedResult[Balance]] = { val filtered = balances.filter(_.available > 0) @@ -57,8 +51,8 @@ class BalancesControllerSpec extends MyAPISpec { Good(result) } - } + val application = guiceApplicationBuilder .overrides(bind[BalanceBlockingDataHandler].to(dataHandler)) .build()