Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,25 @@ abstract class ScanAppReference(
)
}

def getHoldingsSummaryAtV1(
at: CantonTimestamp,
migrationId: Long,
ownerPartyIds: Vector[PartyId] = Vector.empty,
recordTimeMatch: Option[definitions.HoldingsSummaryRequestV1.RecordTimeMatch] = Some(
definitions.HoldingsSummaryRequestV1.RecordTimeMatch.Exact
),
) =
consoleEnvironment.run {
httpCommand(
HttpScanAppClient.GetHoldingsSummaryAtV1(
at.toInstant.atOffset(java.time.ZoneOffset.UTC),
migrationId,
ownerPartyIds,
recordTimeMatch,
)
)
}

def getAggregatedRounds(): Option[ScanAggregator.RoundRange] =
consoleEnvironment.run {
httpCommand(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,24 @@ abstract class ValidatorAppReference(
}
}

def getHoldingsSummaryAtV1(
at: CantonTimestamp,
migrationId: Long,
ownerPartyIds: Vector[PartyId],
recordTimeMatch: Option[definitions.HoldingsSummaryRequestV1.RecordTimeMatch] = None,
): Option[definitions.HoldingsSummaryResponseV1] = {
consoleEnvironment.run {
httpCommand(
HttpScanProxyAppClient.GetHoldingsSummaryAtV1(
at,
migrationId,
ownerPartyIds,
recordTimeMatch,
)
)
}
}

def getDsoInfo(): definitions.GetDsoInfoResponse = {
consoleEnvironment.run {
httpCommand(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ class ScanTimeBasedIntegrationTest
}

"snapshotting" in { implicit env =>
val (aliceUserParty, _) = onboardAliceAndBob()
val (aliceUserParty, bobUserParty) = onboardAliceAndBob()
val migrationId = sv1ScanBackend.config.domainMigrationId

clue(
Expand Down Expand Up @@ -566,6 +566,50 @@ class ScanTimeBasedIntegrationTest
res.summaries.map(_.partyId).distinct shouldBe (Vector(aliceUserParty.toProtoPrimitive))
}

val holdingsSummaryV1 = sv1ScanBackend.getHoldingsSummaryAtV1(
snapshotAfterCts,
migrationId,
ownerPartyIds = Vector(aliceUserParty),
)

inside(holdingsSummaryV1) { case Some(res) =>
res.migrationId should be(migrationId)
res.recordTime should be(snapshotAfter.value)
res.summaries.map(_.partyId).distinct shouldBe (Vector(aliceUserParty.toProtoPrimitive))
forAll(res.summaries) { summary =>
// V1 response should contain non-zero coin totals
BigDecimal(summary.totalCoinHoldings) should be > BigDecimal(0)
BigDecimal(summary.totalCoinHoldings) shouldBe BigDecimal(
summary.totalUnlockedCoin
) + BigDecimal(summary.totalLockedCoin)
}
}

// V1 coin totals should match V0 coin totals
inside((holdingsSummary, holdingsSummaryV1)) { case (Some(v0Res), Some(v1Res)) =>
v0Res.summaries.zip(v1Res.summaries).foreach { case (v0s, v1s) =>
v0s.totalUnlockedCoin shouldBe v1s.totalUnlockedCoin
v0s.totalLockedCoin shouldBe v1s.totalLockedCoin
v0s.totalCoinHoldings shouldBe v1s.totalCoinHoldings
}
}

// V1 multi-party query: querying with both alice and bob exercises
// multi-party support at the HTTP endpoint level
val holdingsSummaryV1MultiParty = sv1ScanBackend.getHoldingsSummaryAtV1(
snapshotAfterCts,
migrationId,
ownerPartyIds = Vector(aliceUserParty, bobUserParty),
)
inside(holdingsSummaryV1MultiParty) { case Some(res) =>
// Alice should appear in the multi-party result with the same values
val aliceSummaries = res.summaries.filter(_.partyId == aliceUserParty.toProtoPrimitive)
aliceSummaries should not be empty
inside(holdingsSummaryV1) { case Some(singleRes) =>
aliceSummaries shouldBe singleRes.summaries
}
}

// afOrBefore should return the same holdingsState and holdingsSummary as the exact time given by snapshotAfter
advanceTime(java.time.Duration.ofMinutes(10))
val atOrBefore = getLedgerTime
Expand Down Expand Up @@ -602,6 +646,21 @@ class ScanTimeBasedIntegrationTest
ownerPartyIds = Vector(aliceUserParty),
recordTimeMatch = Some(definitions.HoldingsSummaryRequest.RecordTimeMatch.Exact),
) shouldBe None

val holdingsSummaryV1AtOrBefore = sv1ScanBackend.getHoldingsSummaryAtV1(
atOrBeforeCts,
migrationId,
ownerPartyIds = Vector(aliceUserParty),
recordTimeMatch = Some(definitions.HoldingsSummaryRequestV1.RecordTimeMatch.AtOrBefore),
)
holdingsSummaryV1AtOrBefore shouldBe holdingsSummaryV1

sv1ScanBackend.getHoldingsSummaryAtV1(
atOrBeforeCts,
migrationId,
ownerPartyIds = Vector(aliceUserParty),
recordTimeMatch = Some(definitions.HoldingsSummaryRequestV1.RecordTimeMatch.Exact),
) shouldBe None
}

advanceTime(java.time.Duration.ofHours(24))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ class ValidatorIntegrationTest extends IntegrationTestWithIsolatedEnvironment wi
Vector(aliceValidatorParty),
)

aliceValidatorBackend.scanProxy.getHoldingsSummaryAtV1(
now,
0L,
Vector(aliceValidatorParty),
) shouldBe sv1ScanBackend.getHoldingsSummaryAtV1(
now,
0L,
Vector(aliceValidatorParty),
)

// check that the dsoGovernance are not vetted
aliceValidatorBackend.participantClient.topology.vetted_packages
.list(filterParticipant = aliceValidatorBackend.participantClient.id.toProtoPrimitive)
Expand Down
119 changes: 117 additions & 2 deletions apps/scan/src/main/openapi/scan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -731,11 +731,12 @@ paths:

/v0/holdings/summary:
post:
tags: [external, scan]
deprecated: true
tags: [deprecated]
x-jvm-package: scan
operationId: "getHoldingsSummaryAt"
description: |
Returns the summary of active amulet contracts for a given migration id and record time, for the given parties.
Deprecated. Please use /v1/holdings/summary instead. 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
Expand All @@ -757,6 +758,36 @@ paths:
"500":
$ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/500"

/v1/holdings/summary:
post:
tags: [external, scan]
x-jvm-package: scan
operationId: "getHoldingsSummaryAtV1"
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.
Unlike /v0/holdings/summary, this version does not include holding fee fields
as they do not express a meaningful aggregate value.
requestBody:
required: true
content:
application/json:
schema:
"$ref": "#/components/schemas/HoldingsSummaryRequestV1"
responses:
"200":
description: ok
content:
application/json:
schema:
$ref: "#/components/schemas/HoldingsSummaryResponseV1"
"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"

/v0/ans-entries:
get:
tags: [external, scan]
Expand Down Expand Up @@ -3229,6 +3260,43 @@ components:
description: |
Compute holding fees as of this round. Defaults to the earliest open mining round.

HoldingsSummaryRequestV1:
type: object
required:
- migration_id
- record_time
- owner_party_ids
properties:
migration_id:
type: integer
format: int64
description: |
The migration id for which to return the summary.
record_time:
type: string
format: date-time
description: |
The timestamp at which the contract set was active.
This needs to be an exact timestamp, i.e.,
needs to correspond to a timestamp reported by `/v0/state/acs/snapshot-timestamp` if `record_time_match` is set to `exact` (which is the default).
If `record_time_match` is set to `at_or_before`, this can be any timestamp, and the most recent snapshot at or before the given `record_time` will be returned.
record_time_match:
type: string
description: |
How to match the record_time. "exact" requires the record_time to match exactly.
"at_or_before" finds the most recent snapshot at or before the given record_time.
enum:
- "exact"
- "at_or_before"
default: "exact"
owner_party_ids:
type: array
items:
type: string
minItems: 1
description: |
The owners for which to compute the summary.

ForceAcsSnapshotResponse:
type: object
required:
Expand Down Expand Up @@ -3379,6 +3447,53 @@ components:
Same as `total_unlocked_coin` - `accumulated_holding_fees_unlocked`.
type: string

HoldingsSummaryResponseV1:
type: object
required:
- record_time
- migration_id
- summaries
properties:
record_time:
description: The same `record_time` as in the request.
type: string
format: date-time
migration_id:
description: The same `migration_id` as in the request.
type: integer
format: int64
summaries:
type: array
items:
$ref: "#/components/schemas/HoldingsSummaryV1"
HoldingsSummaryV1:
description: Aggregate Amulet totals for a particular owner party ID.
type: object
required:
- party_id
- total_unlocked_coin
- total_locked_coin
- total_coin_holdings
properties:
party_id:
description: |
Owner party ID of the amulet. Guaranteed to be unique among `summaries`.
type: string
total_unlocked_coin:
description: |
Sum of unlocked amulet initial amounts, not counting holding
fees deducted since.
type: string
total_locked_coin:
description: |
Sum of locked amulet initial amounts, not
counting holding fees deducted since.
type: string
total_coin_holdings:
description: |
`total_unlocked_coin` + `total_locked_coin`.
type: string

ListActivityRequest:
type: object
required:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ import org.lfdecentralizedtrust.splice.http.HttpClient
import org.lfdecentralizedtrust.splice.http.v0.definitions.{
AnsEntry,
GetDsoInfoResponse,
HoldingsSummaryRequestV1,
HoldingsSummaryResponse,
HoldingsSummaryResponseV1,
LookupTransferCommandStatusResponse,
MigrationSchedule,
}
Expand Down Expand Up @@ -185,6 +187,15 @@ class BftScanConnection(
bftCall(_.getHoldingsSummaryAt(at, migrationId, ownerPartyIds, recordTimeMatch, asOfRound))
}

override def getHoldingsSummaryAtV1(
at: CantonTimestamp,
migrationId: Long,
ownerPartyIds: Vector[PartyId],
recordTimeMatch: Option[HoldingsSummaryRequestV1.RecordTimeMatch],
)(implicit tc: TraceContext): Future[Option[HoldingsSummaryResponseV1]] = {
bftCall(_.getHoldingsSummaryAtV1(at, migrationId, ownerPartyIds, recordTimeMatch))
}

override protected def runGetAmuletRulesWithState(
cachedAmuletRules: Option[ContractWithState[AmuletRules.ContractId, AmuletRules]]
)(implicit tc: TraceContext): Future[ContractWithState[AmuletRules.ContractId, AmuletRules]] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.lfdecentralizedtrust.splice.http.HttpClient
import org.lfdecentralizedtrust.splice.http.v0.definitions.{
GetDsoInfoResponse,
HoldingsSummaryResponse,
HoldingsSummaryResponseV1,
LookupTransferCommandStatusResponse,
MigrationSchedule,
}
Expand All @@ -49,6 +50,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.{
VoteRequest,
}
import org.lfdecentralizedtrust.splice.http.v0.definitions.HoldingsSummaryRequest.RecordTimeMatch
import org.lfdecentralizedtrust.splice.http.v0.definitions.HoldingsSummaryRequestV1

import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future}
import scala.jdk.OptionConverters.*
Expand Down Expand Up @@ -89,6 +91,13 @@ trait ScanConnection
asOfRound: Option[Long],
)(implicit tc: TraceContext): Future[Option[HoldingsSummaryResponse]]

def getHoldingsSummaryAtV1(
at: CantonTimestamp,
migrationId: Long,
ownerPartyIds: Vector[PartyId],
recordTimeMatch: Option[HoldingsSummaryRequestV1.RecordTimeMatch],
)(implicit tc: TraceContext): Future[Option[HoldingsSummaryResponseV1]]

def getAmuletRulesWithState()(implicit
ec: ExecutionContext,
tc: TraceContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ import org.lfdecentralizedtrust.splice.environment.{
}
import org.lfdecentralizedtrust.splice.http.HttpClient
import org.lfdecentralizedtrust.splice.http.v0.definitions.{
HoldingsSummaryRequestV1,
HoldingsSummaryResponse,
HoldingsSummaryResponseV1,
LookupTransferCommandStatusResponse,
MigrationSchedule,
}
Expand Down Expand Up @@ -156,6 +158,23 @@ class SingleScanConnection private[client] (
)
}

override def getHoldingsSummaryAtV1(
at: CantonTimestamp,
migrationId: Long,
ownerPartyIds: Vector[PartyId],
recordTimeMatch: Option[HoldingsSummaryRequestV1.RecordTimeMatch],
)(implicit tc: TraceContext): Future[Option[HoldingsSummaryResponseV1]] = {
runHttpCmd(
config.adminApi.url,
HttpScanAppClient.GetHoldingsSummaryAtV1(
at.toInstant.atOffset(java.time.ZoneOffset.UTC),
migrationId,
ownerPartyIds,
recordTimeMatch,
),
)
}

override def getAmuletRulesWithState()(implicit
ec: ExecutionContext,
tc: TraceContext,
Expand Down
Loading