diff --git a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ValidatorAppReference.scala b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ValidatorAppReference.scala index 9af2e7b5fe..b0a4dc1134 100644 --- a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ValidatorAppReference.scala +++ b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ValidatorAppReference.scala @@ -338,6 +338,27 @@ abstract class ValidatorAppReference( ) } } + + def getHoldingsSummaryAt( + at: CantonTimestamp, + migrationId: Long, + ownerPartyIds: Vector[PartyId], + recordTimeMatch: Option[definitions.HoldingsSummaryRequest.RecordTimeMatch] = None, + asOfRound: Option[Long] = None, + ): Option[definitions.HoldingsSummaryResponse] = { + consoleEnvironment.run { + httpCommand( + HttpScanProxyAppClient.GetHoldingsSummaryAt( + at, + migrationId, + ownerPartyIds, + recordTimeMatch, + asOfRound, + ) + ) + } + } + def getDsoInfo(): definitions.GetDsoInfoResponse = { consoleEnvironment.run { httpCommand( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ValidatorIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ValidatorIntegrationTest.scala index b4298ac281..78c8aef7fb 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ValidatorIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ValidatorIntegrationTest.scala @@ -115,6 +115,17 @@ class ValidatorIntegrationTest extends IntegrationTest with WalletTestUtil { // dso info is available on the scan proxy aliceValidatorBackend.scanProxy.getDsoInfo() shouldBe sv1ScanBackend.getDsoInfo() + val now = env.environment.clock.now + aliceValidatorBackend.scanProxy.getHoldingsSummaryAt( + now, + 0L, + Vector(aliceValidatorParty), + ) shouldBe sv1ScanBackend.getHoldingsSummaryAt( + now, + 0L, + Vector(aliceValidatorParty), + ) + // check that the dsoGovernance are not vetted aliceValidatorBackend.participantClient.topology.vetted_packages .list(filterParticipant = aliceValidatorBackend.participantClient.id.toProtoPrimitive) diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnection.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnection.scala index 70dd2d9ab9..fc2c6ab974 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnection.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/BftScanConnection.scala @@ -33,6 +33,7 @@ import org.lfdecentralizedtrust.splice.http.HttpClient import org.lfdecentralizedtrust.splice.http.v0.definitions.{ AnsEntry, GetDsoInfoResponse, + HoldingsSummaryResponse, LookupTransferCommandStatusResponse, MigrationSchedule, } @@ -91,6 +92,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.{ DsoRules_CloseVoteRequestResult, VoteRequest, } +import org.lfdecentralizedtrust.splice.http.v0.definitions.HoldingsSummaryRequest.RecordTimeMatch import org.lfdecentralizedtrust.tokenstandard.{ allocation, allocationinstruction, @@ -171,6 +173,16 @@ class BftScanConnection( _.getDsoInfo() ) + override def getHoldingsSummaryAt( + at: CantonTimestamp, + migrationId: Long, + ownerPartyIds: Vector[PartyId], + recordTimeMatch: Option[RecordTimeMatch], + asOfRound: Option[Long], + )(implicit tc: TraceContext): Future[Option[HoldingsSummaryResponse]] = { + bftCall(_.getHoldingsSummaryAt(at, migrationId, ownerPartyIds, recordTimeMatch, asOfRound)) + } + override protected def runGetAmuletRulesWithState( cachedAmuletRules: Option[ContractWithState[AmuletRules.ContractId, AmuletRules]] )(implicit tc: TraceContext): Future[ContractWithState[AmuletRules.ContractId, AmuletRules]] = diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/ScanConnection.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/ScanConnection.scala index 1898971f72..1662d8d9b7 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/ScanConnection.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/ScanConnection.scala @@ -22,6 +22,7 @@ import org.lfdecentralizedtrust.splice.environment.* import org.lfdecentralizedtrust.splice.http.HttpClient import org.lfdecentralizedtrust.splice.http.v0.definitions.{ GetDsoInfoResponse, + HoldingsSummaryResponse, LookupTransferCommandStatusResponse, MigrationSchedule, } @@ -35,7 +36,7 @@ import com.digitalasset.canton.data.CantonTimestamp import com.digitalasset.canton.lifecycle.FlagCloseableAsync import com.digitalasset.canton.logging.{NamedLoggerFactory, TracedLogger} import com.digitalasset.canton.time.Clock -import com.digitalasset.canton.topology.{SynchronizerId, PartyId} +import com.digitalasset.canton.topology.{PartyId, SynchronizerId} import com.digitalasset.canton.tracing.TraceContext import io.grpc.Status import org.apache.pekko.stream.Materializer @@ -43,6 +44,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.{ DsoRules_CloseVoteRequestResult, VoteRequest, } +import org.lfdecentralizedtrust.splice.http.v0.definitions.HoldingsSummaryRequest.RecordTimeMatch import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future} import scala.jdk.OptionConverters.* @@ -75,6 +77,14 @@ trait ScanConnection logger, ) + def getHoldingsSummaryAt( + at: CantonTimestamp, + migrationId: Long, + ownerPartyIds: Vector[PartyId], + recordTimeMatch: Option[RecordTimeMatch], + asOfRound: Option[Long], + )(implicit tc: TraceContext): Future[Option[HoldingsSummaryResponse]] + def getAmuletRulesWithState()(implicit ec: ExecutionContext, tc: TraceContext, diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala index c6f43c6198..cb397b1073 100644 --- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala +++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/SingleScanConnection.scala @@ -28,10 +28,11 @@ import org.lfdecentralizedtrust.splice.environment.{ } import org.lfdecentralizedtrust.splice.http.HttpClient import org.lfdecentralizedtrust.splice.http.v0.definitions.{ + HoldingsSummaryResponse, LookupTransferCommandStatusResponse, MigrationSchedule, } -import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.{HttpScanAppClient} +import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.HttpScanAppClient import org.lfdecentralizedtrust.splice.scan.config.ScanAppClientConfig import org.lfdecentralizedtrust.splice.scan.store.db.ScanAggregator import org.lfdecentralizedtrust.splice.store.HistoryBackfilling.SourceMigrationInfo @@ -71,6 +72,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.transferins import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.transferinstructionv1.TransferInstruction import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.allocationv1.Allocation import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.allocationinstructionv1 +import org.lfdecentralizedtrust.splice.http.v0.definitions.HoldingsSummaryRequest.RecordTimeMatch import org.lfdecentralizedtrust.splice.scan.admin.api.client.commands.HttpScanAppClient.BftSequencer import org.lfdecentralizedtrust.tokenstandard.transferinstruction.v1.definitions.TransferFactoryWithChoiceContext @@ -131,6 +133,25 @@ class SingleScanConnection private[client] ( runHttpCmd(config.adminApi.url, HttpScanAppClient.GetDsoInfo(List())) } + override def getHoldingsSummaryAt( + at: CantonTimestamp, + migrationId: Long, + ownerPartyIds: Vector[PartyId], + recordTimeMatch: Option[RecordTimeMatch], + asOfRound: Option[Long], + )(implicit tc: TraceContext): Future[Option[HoldingsSummaryResponse]] = { + runHttpCmd( + config.adminApi.url, + HttpScanAppClient.GetHoldingsSummaryAt( + at.toInstant.atOffset(java.time.ZoneOffset.UTC), + migrationId, + ownerPartyIds, + recordTimeMatch, + asOfRound, + ), + ) + } + override def getAmuletRulesWithState()(implicit ec: ExecutionContext, tc: TraceContext, diff --git a/apps/validator/src/main/openapi/scan-proxy.yaml b/apps/validator/src/main/openapi/scan-proxy.yaml index 961744a4fa..300e15d9a8 100644 --- a/apps/validator/src/main/openapi/scan-proxy.yaml +++ b/apps/validator/src/main/openapi/scan-proxy.yaml @@ -223,7 +223,33 @@ paths: "404": description: No TransferCommand exists with this contract id within the last 24h $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/404" - + /v0/scan-proxy/holdings/summary: + post: + tags: [ scan-proxy ] + x-jvm-package: scanproxy + operationId: "getHoldingsSummaryAt" + description: | + Returns the summary of active amulet contracts for a given migration id and record time, for the given parties. + This is an aggregate of `/v0/holdings/state` by owner party ID with better performance than client-side computation. + requestBody: + required: true + content: + application/json: + schema: + $ref: "../../../../scan/src/main/openapi/scan.yaml#/components/schemas/HoldingsSummaryRequest" + responses: + "200": + description: ok + content: + application/json: + schema: + $ref: "../../../../scan/src/main/openapi/scan.yaml#/components/schemas/HoldingsSummaryResponse" + "400": + $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/400" + "404": + $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/404" + "500": + $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/500" components: schemas: # does not include the TTL, and the contracts are not-MaybeCached diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/api/client/commands/HttpScanProxyAppClient.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/api/client/commands/HttpScanProxyAppClient.scala index 1d7a4d8cbe..d80e098bca 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/api/client/commands/HttpScanProxyAppClient.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/api/client/commands/HttpScanProxyAppClient.scala @@ -5,6 +5,7 @@ package org.lfdecentralizedtrust.splice.validator.admin.api.client.commands import cats.data.EitherT import cats.syntax.either.* +import com.digitalasset.canton.data.CantonTimestamp import org.lfdecentralizedtrust.splice.admin.api.client.commands.HttpCommand import org.lfdecentralizedtrust.splice.codegen.java.splice.ans.AnsRules import org.apache.pekko.http.scaladsl.model.{HttpHeader, HttpResponse, StatusCodes} @@ -57,6 +58,34 @@ object HttpScanProxyAppClient { } } + case class GetHoldingsSummaryAt( + at: CantonTimestamp, + migrationId: Long, + ownerPartyIds: Vector[PartyId], + recordTimeMatch: Option[definitions.HoldingsSummaryRequest.RecordTimeMatch], + asOfRound: Option[Long], + ) extends ScanProxyBaseCommand[scanProxy.GetHoldingsSummaryAtResponse, Option[ + definitions.HoldingsSummaryResponse + ]] { + + override def submitRequest(client: ScanproxyClient, headers: List[HttpHeader]) = + client.getHoldingsSummaryAt( + definitions.HoldingsSummaryRequest( + migrationId, + at.toInstant.atOffset(java.time.ZoneOffset.UTC), + recordTimeMatch, + ownerPartyIds.map(_.toProtoPrimitive), + asOfRound, + ), + headers, + ) + + override def handleOk()(implicit decoder: TemplateJsonDecoder) = { + case scanProxy.GetHoldingsSummaryAtResponse.OK(response) => Right(Some(response)) + case scanProxy.GetHoldingsSummaryAtResponse.NotFound(_) => Right(None) + } + } + case object GetAnsRules extends ScanProxyBaseCommand[ scanProxy.GetAnsRulesResponse, diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpScanProxyHandler.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpScanProxyHandler.scala index f24daff633..b0d3c321f3 100644 --- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpScanProxyHandler.scala +++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpScanProxyHandler.scala @@ -3,6 +3,7 @@ package org.lfdecentralizedtrust.splice.validator.admin.http +import com.digitalasset.canton.data.CantonTimestamp import org.lfdecentralizedtrust.splice.http.v0.definitions.MaybeCachedContractWithState import org.lfdecentralizedtrust.splice.http.v0.{definitions, scanproxy as v0} import org.lfdecentralizedtrust.splice.scan.admin.api.client.BftScanConnection @@ -99,6 +100,31 @@ class HttpScanProxyHandler( } } + override def getHoldingsSummaryAt( + respond: v0.ScanproxyResource.GetHoldingsSummaryAtResponse.type + )( + body: definitions.HoldingsSummaryRequest + )(tUser: AuthenticatedRequest): Future[v0.ScanproxyResource.GetHoldingsSummaryAtResponse] = { + implicit val AuthenticatedRequest(_, traceContext) = tUser + withSpan(s"$workflowId.getHoldingsSummaryAt") { implicit traceContext => _ => + for { + summaryOpt <- scanConnection.getHoldingsSummaryAt( + CantonTimestamp.assertFromInstant(body.recordTime.toInstant), + body.migrationId, + body.ownerPartyIds.map(PartyId.tryFromProtoPrimitive), + body.recordTimeMatch, + body.asOfRound, + ) + } yield { + summaryOpt match { + case Some(summary) => respond.OK(summary) + case None => + respond.NotFound(definitions.ErrorResponse("Summary not found for given parameters")) + } + } + } + } + override def getAmuletRules(respond: v0.ScanproxyResource.GetAmuletRulesResponse.type)()( tUser: AuthenticatedRequest ): Future[v0.ScanproxyResource.GetAmuletRulesResponse] = { diff --git a/docs/src/release_notes.rst b/docs/src/release_notes.rst index 32c2bcf47a..d2d9786efa 100644 --- a/docs/src/release_notes.rst +++ b/docs/src/release_notes.rst @@ -102,6 +102,10 @@ - The default logger has been switched to use an asynchronous appender, for all the nodes, for better performance. The behavior can be switched back to synchronous logging by setting the environment variable `LOG_IMMEDIATE_FLUSH=true`. + - Validator + + - Expose ``/v0/holdings/summary`` endpoint from scan proxy. + .. release-notes:: 0.5.6 - Sequencer