Skip to content

Commit

Permalink
Add time interval query params to POST /addresses/transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
tdroxler committed Oct 1, 2024
1 parent b0a857e commit 13c80b7
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 16 deletions.
22 changes: 22 additions & 0 deletions app/src/main/resources/explorer-backend-openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -5340,6 +5340,28 @@
"description": "List transactions for given addresses",
"operationId": "postAddressesTransactions",
"parameters": [
{
"schema": {
"format": "int64",
"type": "integer",
"minimum": "0"
},
"in": "query",
"name": "fromTs",
"description": "inclusive",
"required": false
},
{
"schema": {
"format": "int64",
"type": "integer",
"minimum": "0"
},
"in": "query",
"name": "toTs",
"description": "exclusive",
"required": false
},
{
"schema": {
"format": "int32",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import org.alephium.explorer.api.EndpointExamples._
import org.alephium.explorer.api.model._
import org.alephium.protocol.PublicKey
import org.alephium.protocol.model.{Address, TokenId}
import org.alephium.util.Duration
import org.alephium.util.{Duration, TimeStamp}

// scalastyle:off magic.number
trait AddressesEndpoints extends BaseEndpoint with QueryParams {
Expand Down Expand Up @@ -73,14 +73,16 @@ trait AddressesEndpoints extends BaseEndpoint with QueryParams {
.out(jsonBody[ArraySeq[Transaction]])
.description("List transactions of a given address")

lazy val getTransactionsByAddresses
: BaseEndpoint[(ArraySeq[Address], Pagination), ArraySeq[Transaction]] =
// format: off
lazy val getTransactionsByAddresses: BaseEndpoint[(ArraySeq[Address], Option[TimeStamp], Option[TimeStamp], Pagination), ArraySeq[Transaction]] =
baseAddressesEndpoint.post
.in(arrayBody[Address]("addresses", maxSizeAddresses))
.in("transactions")
.in(optionalTimeIntervalQuery)
.in(pagination)
.out(jsonBody[ArraySeq[Transaction]])
.description("List transactions for given addresses")
// format: on

val getTransactionsByAddressTimeRanged
: BaseEndpoint[(Address, TimeInterval, Pagination), ArraySeq[Transaction]] =
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/scala/org/alephium/explorer/api/QueryParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ trait QueryParams extends TapirCodecs {
}
})

val optionalTimeIntervalQuery: EndpointInput[(Option[TimeStamp], Option[TimeStamp])] =
query[Option[TimeStamp]]("fromTs")
.description("inclusive")
.and(query[Option[TimeStamp]]("toTs").description("exclusive"))
.validate(Validator.custom {
case (Some(fromTs), Some(toTs)) if fromTs >= toTs =>
ValidationResult.Invalid(s"`fromTs` must be before `toTs`")
case _ =>
ValidationResult.Valid
})

val intervalTypeQuery: EndpointInput[IntervalType] =
query[IntervalType]("interval-type")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,16 @@ object TransactionDao {
): Future[ArraySeq[Transaction]] =
run(getTransactionsByAddress(address, pagination))

def getByAddresses(addresses: ArraySeq[Address], pagination: Pagination)(implicit
def getByAddresses(
addresses: ArraySeq[Address],
fromTime: Option[TimeStamp],
toTime: Option[TimeStamp],
pagination: Pagination
)(implicit
ec: ExecutionContext,
dc: DatabaseConfig[PostgresProfile]
): Future[ArraySeq[Transaction]] =
run(getTransactionsByAddresses(addresses, pagination))
run(getTransactionsByAddresses(addresses, fromTime, toTime, pagination))

def getByAddressTimeRanged(
address: Address,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,17 @@ object TransactionQueries extends StrictLogging {
* Page number (starting from 0)
* @param limit
* Maximum rows
* @param fromTs
* From TimeStamp of the time-range (inclusive)
* @param toTs
* To TimeStamp of the time-range (exclusive)
* @return
* Paginated transactions
*/
def getTxHashesByAddressesQuery(
addresses: ArraySeq[Address],
fromTs: Option[TimeStamp],
toTs: Option[TimeStamp],
pagination: Pagination
): DBActionSR[TxByAddressQR] =
if (addresses.isEmpty) {
Expand All @@ -200,6 +206,8 @@ object TransactionQueries extends StrictLogging {
FROM transaction_per_addresses
WHERE main_chain = true
AND address IN $placeholder
${fromTs.map(ts => s"AND block_timestamp >= ${ts.millis}").getOrElse("")}
${toTs.map(ts => s"AND block_timestamp < ${ts.millis}").getOrElse("")}
ORDER BY block_timestamp DESC, tx_order
"""

Expand Down Expand Up @@ -273,11 +281,16 @@ object TransactionQueries extends StrictLogging {
} yield txs
}

def getTransactionsByAddresses(addresses: ArraySeq[Address], pagination: Pagination)(implicit
def getTransactionsByAddresses(
addresses: ArraySeq[Address],
fromTime: Option[TimeStamp],
toTime: Option[TimeStamp],
pagination: Pagination
)(implicit
ec: ExecutionContext
): DBActionR[ArraySeq[Transaction]] = {
for {
txHashesTs <- getTxHashesByAddressesQuery(addresses, pagination)
txHashesTs <- getTxHashesByAddressesQuery(addresses, fromTime, toTime, pagination)
txs <- getTransactions(txHashesTs)
} yield txs
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ trait TransactionService {
dc: DatabaseConfig[PostgresProfile]
): Future[ArraySeq[Transaction]]

def getTransactionsByAddresses(addresses: ArraySeq[Address], pagination: Pagination)(implicit
def getTransactionsByAddresses(
addresses: ArraySeq[Address],
fromTime: Option[TimeStamp],
toTime: Option[TimeStamp],
pagination: Pagination
)(implicit
ec: ExecutionContext,
dc: DatabaseConfig[PostgresProfile]
): Future[ArraySeq[Transaction]]
Expand Down Expand Up @@ -157,11 +162,16 @@ object TransactionService extends TransactionService {
): Future[ArraySeq[Transaction]] =
TransactionDao.getByAddressTimeRanged(address, fromTime, toTime, pagination)

def getTransactionsByAddresses(addresses: ArraySeq[Address], pagination: Pagination)(implicit
def getTransactionsByAddresses(
addresses: ArraySeq[Address],
fromTime: Option[TimeStamp],
toTime: Option[TimeStamp],
pagination: Pagination
)(implicit
ec: ExecutionContext,
dc: DatabaseConfig[PostgresProfile]
): Future[ArraySeq[Transaction]] =
TransactionDao.getByAddresses(addresses, pagination)
TransactionDao.getByAddresses(addresses, fromTime, toTime, pagination)

def listMempoolTransactionsByAddress(address: Address)(implicit
ec: ExecutionContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ class AddressServer(
transactionService
.getTransactionsByAddress(address, pagination)
}),
route(getTransactionsByAddresses.serverLogicSuccess[Future] { case (addresses, pagination) =>
transactionService
.getTransactionsByAddresses(addresses, pagination)
route(getTransactionsByAddresses.serverLogicSuccess[Future] {
case (addresses, fromTsOpt, toTsOpt, pagination) =>
transactionService
.getTransactionsByAddresses(addresses, fromTsOpt, toTsOpt, pagination)
}),
route(getTransactionsByAddressTimeRanged.serverLogicSuccess[Future] {
case (address, timeInterval, pagination) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@ class TransactionQueriesSpec extends AlephiumFutureSpec with DatabaseFixtureForE
val query =
TransactionQueries.getTxHashesByAddressesQuery(
addresses,
None,
None,
Pagination.unsafe(page, limit)
)

Expand Down Expand Up @@ -536,6 +538,8 @@ class TransactionQueriesSpec extends AlephiumFutureSpec with DatabaseFixtureForE
val query =
TransactionQueries.getTxHashesByAddressesQuery(
shuffledAddresses,
None,
None,
Pagination.unsafe(1, Int.MaxValue)
)

Expand All @@ -561,7 +565,6 @@ class TransactionQueriesSpec extends AlephiumFutureSpec with DatabaseFixtureForE
forAll(Gen.listOf(genTransactionPerAddressEntity(mainChain = Gen.const(true)))) {
entities =>
val toPersist = entities.flatMap { entity =>
// We copy the entity and change the address
Seq(entity, entity.copy(address = addressGen.sample.get))
}

Expand All @@ -572,6 +575,8 @@ class TransactionQueriesSpec extends AlephiumFutureSpec with DatabaseFixtureForE
val query =
TransactionQueries.getTxHashesByAddressesQuery(
toPersist.map(_.address),
None,
None,
Pagination.unsafe(1, Int.MaxValue)
)

Expand All @@ -590,6 +595,76 @@ class TransactionQueriesSpec extends AlephiumFutureSpec with DatabaseFixtureForE
}
}
}

"return timed range transactions" in new Fixture {
forAll(
Gen.nonEmptyListOf(
genTransactionPerAddressEntity(
addressGen = Gen.const(address),
mainChain = Gen.const(true)
)
)
) { entities =>
run(TransactionPerAddressSchema.table.delete).futureValue
run(TransactionPerAddressSchema.table ++= entities).futureValue

val timestamps = entities.map(_.timestamp).distinct
val max = timestamps.max
val min = timestamps.min

def query(fromTs: Option[TimeStamp], toTs: Option[TimeStamp]) = {
run(
TransactionQueries.getTxHashesByAddressesQuery(
Seq(address),
fromTs,
toTs,
Pagination.unsafe(1, Int.MaxValue)
)
).futureValue
}

def expected(entities: Seq[TransactionPerAddressEntity]) = {
entities.map { entity =>
TxByAddressQR(
entity.hash,
entity.blockHash,
entity.timestamp,
entity.txOrder,
entity.coinbase
)
}
}

def test(
fromTs: Option[TimeStamp],
toTs: Option[TimeStamp],
expectedEntites: Seq[TxByAddressQR]
) = {
query(fromTs, toTs) should contain theSameElementsAs expectedEntites
}

// fromTs is inclusive
test(fromTs = Some(max), toTs = None, expected(Seq(entities.maxBy(_.timestamp))))

// toTs is exclusive
test(fromTs = None, toTs = Some(max), expected(entities.sortBy(_.timestamp).init))

// Verifying max+1 include the last element
test(
fromTs = None,
toTs = Some(max.plusMillisUnsafe(1)),
expected(entities.sortBy(_.timestamp))
)

// excluding min and max elememt
test(
fromTs = Some(min.plusMillisUnsafe(1)),
toTs = Some(max),
expected(entities.sortBy(_.timestamp).init.drop(1))
)

}
}
}

trait Fixture {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,12 @@ trait EmptyTransactionService extends TransactionService {
): Future[ArraySeq[Transaction]] =
Future.successful(ArraySeq.empty)

override def getTransactionsByAddresses(addresses: ArraySeq[Address], pagination: Pagination)(
implicit
override def getTransactionsByAddresses(
addresses: ArraySeq[Address],
fromTs: Option[TimeStamp],
toTs: Option[TimeStamp],
pagination: Pagination
)(implicit
ec: ExecutionContext,
dc: DatabaseConfig[PostgresProfile]
): Future[ArraySeq[Transaction]] =
Expand Down

0 comments on commit 13c80b7

Please sign in to comment.