diff --git a/apps/common/src/main/resources/db/migration/canton-network/postgres/stable/V066__scan_rewards_reference_round_index.sql b/apps/common/src/main/resources/db/migration/canton-network/postgres/stable/V066__scan_rewards_reference_round_index.sql new file mode 100644 index 0000000000..0ed59cbe7d --- /dev/null +++ b/apps/common/src/main/resources/db/migration/canton-network/postgres/stable/V066__scan_rewards_reference_round_index.sql @@ -0,0 +1,11 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +-- Index on round column for efficient lookups by round number. +create index scan_rewards_reference_store_active_round + on scan_rewards_reference_store_active (store_id, migration_id, round) + where round is not null; + +create index scan_rewards_reference_store_archived_round + on scan_rewards_reference_store_archived (store_id, migration_id, round) + where round is not null; diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanRewardsReferenceStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanRewardsReferenceStore.scala index b0239fc708..b5d4e4d8d9 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanRewardsReferenceStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanRewardsReferenceStore.scala @@ -8,7 +8,9 @@ import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.tracing.TraceContext import com.github.blemale.scaffeine.Scaffeine +import org.lfdecentralizedtrust.splice.codegen.java.splice.round.OpenMiningRound import org.lfdecentralizedtrust.splice.store.{Limit, MultiDomainAcsStore, SynchronizerStore} +import org.lfdecentralizedtrust.splice.util.Contract import scala.concurrent.{ExecutionContext, Future} @@ -50,6 +52,13 @@ class CachingScanRewardsReferenceStore private[splice] ( )(implicit tc: TraceContext): Future[Set[String]] = featuredAppPartiesCache.get(asOf) + override def lookupOpenMiningRoundByNumber( + roundNumber: Long + )(implicit + tc: TraceContext + ): Future[Option[Contract[OpenMiningRound.ContractId, OpenMiningRound]]] = + store.lookupOpenMiningRoundByNumber(roundNumber) + override val storeName: String = store.storeName override def defaultLimit: Limit = store.defaultLimit override lazy val acsContractFilter = store.acsContractFilter diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanRewardsReferenceStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanRewardsReferenceStore.scala index b55ed62aa8..b816c1a04b 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanRewardsReferenceStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanRewardsReferenceStore.scala @@ -11,13 +11,14 @@ import com.digitalasset.canton.resource.DbStorage import com.digitalasset.canton.topology.{ParticipantId, PartyId, SynchronizerId} import com.digitalasset.canton.tracing.TraceContext import org.lfdecentralizedtrust.splice.codegen.java.splice +import org.lfdecentralizedtrust.splice.codegen.java.splice.round.OpenMiningRound import org.lfdecentralizedtrust.splice.config.IngestionConfig import org.lfdecentralizedtrust.splice.environment.RetryProvider import org.lfdecentralizedtrust.splice.migration.DomainMigrationInfo import org.lfdecentralizedtrust.splice.scan.store.db.ScanRewardsReferenceTables.ScanRewardsReferenceStoreRowData import org.lfdecentralizedtrust.splice.store.{AppStore, Limit, MultiDomainAcsStore} import org.lfdecentralizedtrust.splice.store.db.AcsInterfaceViewRowData -import org.lfdecentralizedtrust.splice.util.TemplateJsonDecoder +import org.lfdecentralizedtrust.splice.util.{Contract, TemplateJsonDecoder} import scala.concurrent.{ExecutionContext, Future} @@ -60,6 +61,16 @@ trait ScanRewardsReferenceStore extends AppStore { asOf: CantonTimestamp )(implicit tc: TraceContext): Future[Set[String]] + /** Look up an OpenMiningRound contract by its round number. + * Checks both the active ACS table and the archive table, + * since the round may have already been closed by the time the trigger runs. + */ + def lookupOpenMiningRoundByNumber( + roundNumber: Long + )(implicit + tc: TraceContext + ): Future[Option[Contract[OpenMiningRound.ContractId, OpenMiningRound]]] + override lazy val acsContractFilter: MultiDomainAcsStore.ContractFilter[ ScanRewardsReferenceStoreRowData, AcsInterfaceViewRowData.NoInterfacesIngested, diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanRewardsReferenceStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanRewardsReferenceStore.scala index 2cd8be9831..a7de128f63 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanRewardsReferenceStore.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanRewardsReferenceStore.scala @@ -18,11 +18,21 @@ import org.lfdecentralizedtrust.splice.scan.store.ScanRewardsReferenceStore import org.lfdecentralizedtrust.splice.store.{Limit, TcsStore} import org.lfdecentralizedtrust.splice.store.db.{ AcsArchiveConfig, + AcsQueries, + AcsTables, DbAppStore, DbTcsStore, StoreDescriptor, } -import org.lfdecentralizedtrust.splice.util.{ContractWithState, TemplateJsonDecoder} +import org.lfdecentralizedtrust.splice.store.db.AcsQueries.SelectFromAcsTableResult +import org.lfdecentralizedtrust.splice.util.{ + Contract, + ContractWithState, + PackageQualifiedName, + TemplateJsonDecoder, +} +import org.lfdecentralizedtrust.splice.util.FutureUnlessShutdownUtil.futureUnlessShutdownToFuture +import slick.jdbc.canton.ActionBasedSQLInterpolation.Implicits.actionBasedSQLInterpolationCanton import scala.concurrent.{ExecutionContext, Future} @@ -62,7 +72,9 @@ class DbScanRewardsReferenceStore( ) ), ) - with ScanRewardsReferenceStore { + with ScanRewardsReferenceStore + with AcsTables + with AcsQueries { override def waitUntilInitialized: Future[Unit] = multiDomainAcsStore.waitUntilAcsIngested() @@ -137,4 +149,47 @@ class DbScanRewardsReferenceStore( tc: TraceContext ): Future[Seq[ContractWithState[OpenMiningRound.ContractId, OpenMiningRound]]] = tcsStore.listAllContractsAsOf(OpenMiningRound.COMPANION, asOf) + + override def lookupOpenMiningRoundByNumber( + roundNumber: Long + )(implicit + tc: TraceContext + ): Future[Option[Contract[OpenMiningRound.ContractId, OpenMiningRound]]] = + waitUntilInitialized.flatMap { _ => + lookupOpenMiningRoundByNumberQuery(roundNumber) + } + + private def lookupOpenMiningRoundByNumberQuery( + roundNumber: Long + )(implicit + tc: TraceContext + ): Future[Option[Contract[OpenMiningRound.ContractId, OpenMiningRound]]] = { + val storeId = multiDomainAcsStore.acsStoreId + val migrationId = multiDomainAcsStore.domainMigrationId + val pqn = PackageQualifiedName.fromJavaCodegenCompanion(OpenMiningRound.COMPANION) + val columns = SelectFromAcsTableResult.sqlColumnsCommaSeparated() + val query = + sql"""( + select #$columns + from #${ScanRewardsReferenceTables.acsTableName} acs + where acs.store_id = $storeId + and acs.migration_id = $migrationId + and acs.package_name = ${pqn.packageName} + and acs.template_id_qualified_name = ${pqn.qualifiedName} + and acs.round = $roundNumber + ) union all ( + select #$columns + from #${ScanRewardsReferenceTables.archiveTableName} acs + where acs.store_id = $storeId + and acs.migration_id = $migrationId + and acs.package_name = ${pqn.packageName} + and acs.template_id_qualified_name = ${pqn.qualifiedName} + and acs.round = $roundNumber + ) limit 1""".as[SelectFromAcsTableResult] + for { + result <- futureUnlessShutdownToFuture( + storage.query(query, "lookupOpenMiningRoundByNumber") + ) + } yield result.headOption.map(contractFromRow(OpenMiningRound.COMPANION)(_)) + } } diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/DbScanRewardsReferenceStoreTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/DbScanRewardsReferenceStoreTest.scala index 2677c7d890..7901ada14e 100644 --- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/DbScanRewardsReferenceStoreTest.scala +++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/DbScanRewardsReferenceStoreTest.scala @@ -180,6 +180,48 @@ class DbScanRewardsReferenceStoreTest } yield succeed } + "lookupOpenMiningRoundByNumber returns the correct contract" in { + val store = mkStore() + val omr3 = openMiningRound(dsoParty, round = 3, amuletPrice = 1.0) + .copy(createdAt = CantonTimestamp.ofEpochSecond(100).toInstant) + val omr4 = openMiningRound(dsoParty, round = 4, amuletPrice = 1.5) + .copy(createdAt = CantonTimestamp.ofEpochSecond(200).toInstant) + val omr5 = openMiningRound(dsoParty, round = 5, amuletPrice = 2.0) + .copy(createdAt = CantonTimestamp.ofEpochSecond(300).toInstant) + for { + _ <- initWithAcs()(store.multiDomainAcsStore) + _ <- sync1.create(omr3, recordTime = CantonTimestamp.ofEpochSecond(100).toInstant)( + store.multiDomainAcsStore + ) + _ <- sync1.create(omr4, recordTime = CantonTimestamp.ofEpochSecond(200).toInstant)( + store.multiDomainAcsStore + ) + _ <- sync1.create(omr5, recordTime = CantonTimestamp.ofEpochSecond(300).toInstant)( + store.multiDomainAcsStore + ) + // Archive round 3 — it should still be found in the archive table + _ <- sync1.archive(omr3, recordTime = CantonTimestamp.ofEpochSecond(350).toInstant)( + store.multiDomainAcsStore + ) + + // Round 3: archived — found via archive table + result3 <- store.lookupOpenMiningRoundByNumber(3) + _ = result3 shouldBe Some(omr3) + + // Round 4: still active — found via active table + result4 <- store.lookupOpenMiningRoundByNumber(4) + _ = result4 shouldBe Some(omr4) + + // Round 5: still active + result5 <- store.lookupOpenMiningRoundByNumber(5) + _ = result5 shouldBe Some(omr5) + + // Round 99: never existed + resultMissing <- store.lookupOpenMiningRoundByNumber(99) + _ = resultMissing shouldBe None + } yield succeed + } + "lookupActiveOpenMiningRounds" in { val store = mkStore() // Timeline (ingestion start = 250, earliest archived_at): diff --git a/project/BuildCommon.scala b/project/BuildCommon.scala index a0530d5626..109411a519 100644 --- a/project/BuildCommon.scala +++ b/project/BuildCommon.scala @@ -250,8 +250,8 @@ object BuildCommon { "splice-util-token-standard-wallet-test-daml/clean", "splice-util-batched-markers-daml/clean", "splice-util-batched-markers-test-daml/clean", - "splice-featured-app-api-v1-daml/clean", - "splice-featured-app-api-v2-daml/clean", + "splice-api-featured-app-v1-daml/clean", + "splice-api-featured-app-v2-daml/clean", ).map(";" + _).mkString(""), ) ++ addCommandAlias("splice-clean", "; clean-splice") ++