Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve post addresses transactions #568

Merged
merged 2 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -5470,6 +5470,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 @@ -196,10 +202,12 @@ object TransactionQueries extends StrictLogging {

val query =
s"""
SELECT ${TxByAddressQR.selectFields}
SELECT DISTINCT ${TxByAddressQR.selectFields}
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 @@ -68,7 +68,12 @@ trait TransactionService {
dc: DatabaseConfig[PostgresProfile]
): Future[Option[TransactionInfo]]

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 @@ -168,11 +173,16 @@ object TransactionService extends TransactionService {
): Future[Option[TransactionInfo]] =
TransactionDao.getLatestTransactionInfoByAddress(address)

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 @@ -555,6 +559,112 @@ class TransactionQueriesSpec extends AlephiumFutureSpec with DatabaseFixtureForE
}
}
}

"return distinct transactions" when {
"a transaction is associated with multiple addresses" in {
forAll(Gen.listOf(genTransactionPerAddressEntity(mainChain = Gen.const(true)))) {
entities =>
val toPersist = entities.flatMap { entity =>
Seq(entity, entity.copy(address = addressGen.sample.get))
}

// clear table and insert entities
run(TransactionPerAddressSchema.table.delete).futureValue
run(TransactionPerAddressSchema.table ++= toPersist).futureValue

val query =
TransactionQueries.getTxHashesByAddressesQuery(
toPersist.map(_.address),
None,
None,
Pagination.unsafe(1, Int.MaxValue)
)

val expectedResult =
entities map { entity =>
TxByAddressQR(
entity.hash,
entity.blockHash,
entity.timestamp,
entity.txOrder,
entity.coinbase
)
}

run(query).futureValue should contain theSameElementsAs expectedResult
}
}
}

"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
Loading