diff --git a/.github/workflows/build.deployment_test.yml b/.github/workflows/build.deployment_test.yml
index 2652c6b445..e7705bb0ed 100644
--- a/.github/workflows/build.deployment_test.yml
+++ b/.github/workflows/build.deployment_test.yml
@@ -25,7 +25,7 @@ jobs:
- name: Setup
uses: ./.github/actions/tests/common_test_setup
with:
- cache_version: 8
+ cache_version: 9
test_name: deployment_test
with_sbt: false
diff --git a/.github/workflows/build.docs.yml b/.github/workflows/build.docs.yml
index 11311ee26e..362a4cf039 100644
--- a/.github/workflows/build.docs.yml
+++ b/.github/workflows/build.docs.yml
@@ -25,7 +25,7 @@ jobs:
id: setup
uses: ./.github/actions/tests/common_test_setup
with:
- cache_version: 8
+ cache_version: 9
test_name: docs
save_nix_cache: true
save_nix_cache_to_gcp: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
@@ -48,7 +48,7 @@ jobs:
- name: Post-SBT job
uses: ./.github/actions/sbt/post_sbt
with:
- cache_version: 8
+ cache_version: 9
setup_sbt_cache_hits: ${{ steps.setup.outputs.sbt_cache_hits }}
- name: Report Failures on Slack & Github
diff --git a/.github/workflows/build.scala_test.yml b/.github/workflows/build.scala_test.yml
index 855939556c..5f0434d110 100644
--- a/.github/workflows/build.scala_test.yml
+++ b/.github/workflows/build.scala_test.yml
@@ -137,7 +137,7 @@ jobs:
- name: Run Tests
uses: ./.github/actions/tests/scala_test
with:
- cache_version: 8
+ cache_version: 9
with_canton: ${{ inputs.with_canton }}
start_canton_options: ${{ inputs.start_canton_options }}
test_suite_name: ${{ inputs.test_name }}
diff --git a/.github/workflows/build.scala_test_for_compose.yml b/.github/workflows/build.scala_test_for_compose.yml
index 1c8d0d21ae..b348d7723c 100644
--- a/.github/workflows/build.scala_test_for_compose.yml
+++ b/.github/workflows/build.scala_test_for_compose.yml
@@ -104,7 +104,7 @@ jobs:
- name: Run Tests
uses: ./.github/actions/tests/scala_test
with:
- cache_version: 8
+ cache_version: 9
start_canton_options: ${{ inputs.start_canton_options }}
test_suite_name: ${{ inputs.test_name }}
test_names: ${{ needs.split_tests.outputs.test_names }}
diff --git a/.github/workflows/build.scala_test_with_cometbft.yml b/.github/workflows/build.scala_test_with_cometbft.yml
index 76a5512c1e..6434e79ee7 100644
--- a/.github/workflows/build.scala_test_with_cometbft.yml
+++ b/.github/workflows/build.scala_test_with_cometbft.yml
@@ -120,7 +120,7 @@ jobs:
- name: Run Tests
uses: ./.github/actions/tests/scala_test
with:
- cache_version: 8
+ cache_version: 9
start_canton_options: -F -w
test_suite_name: ${{ inputs.test_name }}
test_names: ${{ needs.split_tests.outputs.test_names }}
diff --git a/.github/workflows/build.static_tests.yml b/.github/workflows/build.static_tests.yml
index 4820b0ad5e..6cfa6d7584 100644
--- a/.github/workflows/build.static_tests.yml
+++ b/.github/workflows/build.static_tests.yml
@@ -31,7 +31,7 @@ jobs:
- name: Setup Nix
uses: ./.github/actions/tests/common_test_setup
with:
- cache_version: 8
+ cache_version: 9
test_name: canton_consistency
target: 'static_tests'
@@ -84,7 +84,7 @@ jobs:
id: setup
uses: ./.github/actions/tests/common_test_setup
with:
- cache_version: 8
+ cache_version: 9
test_name: static_tests
target: 'static_tests'
diff --git a/.github/workflows/build.ts_cli_tests.yml b/.github/workflows/build.ts_cli_tests.yml
index c746a821ff..1ddf0f3bf9 100644
--- a/.github/workflows/build.ts_cli_tests.yml
+++ b/.github/workflows/build.ts_cli_tests.yml
@@ -41,7 +41,7 @@ jobs:
if: steps.skip.outputs.skip != 'true'
uses: ./.github/actions/tests/common_test_setup
with:
- cache_version: 8
+ cache_version: 9
test_name: ts_cli
- name: Run Token Standard CLI tests
diff --git a/.github/workflows/build.ui_tests.yml b/.github/workflows/build.ui_tests.yml
index 27742c3134..aa1b5b5855 100644
--- a/.github/workflows/build.ui_tests.yml
+++ b/.github/workflows/build.ui_tests.yml
@@ -41,7 +41,7 @@ jobs:
if: steps.skip.outputs.skip != 'true'
uses: ./.github/actions/tests/common_test_setup
with:
- cache_version: 8
+ cache_version: 9
test_name: ui_tests
- name: Run UI tests
@@ -62,7 +62,7 @@ jobs:
if: steps.skip.outputs.skip != 'true'
uses: ./.github/actions/sbt/post_sbt
with:
- cache_version: 8
+ cache_version: 9
setup_sbt_cache_hits: ${{ steps.setup.outputs.sbt_cache_hits }}
- name: Upload logs
diff --git a/.github/workflows/bump_gha_runner_version.yml b/.github/workflows/bump_gha_runner_version.yml
index 8556944a43..b6e3838256 100644
--- a/.github/workflows/bump_gha_runner_version.yml
+++ b/.github/workflows/bump_gha_runner_version.yml
@@ -27,7 +27,7 @@ jobs:
- name: Set up Nix (Self hosted)
uses: ./.github/actions/nix/setup_nix
with:
- cache_version: 8
+ cache_version: 9
target: default
- name: Check for the latest version and create a PR to splice
diff --git a/.github/workflows/performance_tests.yml b/.github/workflows/performance_tests.yml
index 12895fe0b8..caf66cf18d 100644
--- a/.github/workflows/performance_tests.yml
+++ b/.github/workflows/performance_tests.yml
@@ -34,7 +34,7 @@ jobs:
id: setup
uses: ./.github/actions/tests/common_test_setup
with:
- cache_version: 8
+ cache_version: 9
test_name: ingestion_performance_tests
# Authenticate to GCP for read access to GCS
@@ -137,7 +137,7 @@ jobs:
id: setup
uses: ./.github/actions/tests/common_test_setup
with:
- cache_version: 8
+ cache_version: 9
test_name: read_performance_tests
# Authenticate to GCP for read access to GCS
diff --git a/.github/workflows/pr_check_github_scripts.yml b/.github/workflows/pr_check_github_scripts.yml
index 491bbacd5c..23f6125797 100644
--- a/.github/workflows/pr_check_github_scripts.yml
+++ b/.github/workflows/pr_check_github_scripts.yml
@@ -15,7 +15,7 @@ jobs:
- name: Set up Nix
uses: ./.github/actions/nix/setup_nix
with:
- cache_version: 8
+ cache_version: 9
- name: Check github scripts
uses: ./.github/actions/nix/run_bash_command_in_nix
with:
diff --git a/.gitignore b/.gitignore
index 2c56be64b4..fae017aaa8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,8 @@ _build/
**/metals.sbt
**/.scala-build/*
+daml/daml-ide-mono/daml/
+daml/daml-ide-mono/.vscode/
.vscode/*
!.vscode/settings.json
# Make sure test files are checkedin
diff --git a/apps/app/src/main/resources/application.conf b/apps/app/src/main/resources/application.conf
index ef8ce7ae4b..52931b943c 100644
--- a/apps/app/src/main/resources/application.conf
+++ b/apps/app/src/main/resources/application.conf
@@ -2,7 +2,8 @@
pekko.http.server.request-timeout = 38 seconds
pekko.http.server.parsing.error-handler="org.lfdecentralizedtrust.splice.http.PekkoHttpParsingErrorHandler$"
pekko.http.server.parsing.ignore-illegal-header-for =
- [ "authorization"
+ [ "accept-language"
+ "authorization"
"cookie"
"origin"
"proxy-authorization"
diff --git a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ScanAppReference.scala b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ScanAppReference.scala
index e510e5b33a..8de8c4fe8e 100644
--- a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ScanAppReference.scala
+++ b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ScanAppReference.scala
@@ -368,20 +368,6 @@ abstract class ScanAppReference(
httpCommand(HttpScanAppClient.GetRewardAccountingBatch(roundNumber, batchHash))
}
- import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryResponseItem
- import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryRequest.SortOrder
-
- def listTransactions(
- pageEndEventId: Option[String],
- sortOrder: SortOrder,
- pageSize: Int,
- ): Seq[TransactionHistoryResponseItem] =
- consoleEnvironment.run {
- httpCommand(
- HttpScanAppClient.ListTransactions(pageEndEventId, sortOrder, pageSize)
- )
- }
-
def getAcsSnapshot(party: PartyId, recordTime: Option[Instant]): ByteString =
consoleEnvironment.run {
httpCommand(
diff --git a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/SvAppReference.scala b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/SvAppReference.scala
index 37516b4b28..9f9083697c 100644
--- a/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/SvAppReference.scala
+++ b/apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/SvAppReference.scala
@@ -134,7 +134,7 @@ abstract class SvAppReference(
@Help.Summary("Cancel a running logical synchronizer upgrade by removing its LSU announcement")
def cancelLogicalSynchronizerUpgrade(): Unit =
consoleEnvironment.run {
- httpCommand(HttpSvAdminAppClient.CancelLogicalSynchronizerUpgrade())
+ httpCommand(HttpSvOperatorAppClient.CancelLogicalSynchronizerUpgrade())
}
@Help.Summary("Get identities of all domain node components")
diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/EnvironmentDefinition.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/EnvironmentDefinition.scala
index b28ff145d3..7ff16ddb2b 100644
--- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/EnvironmentDefinition.scala
+++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/EnvironmentDefinition.scala
@@ -504,6 +504,13 @@ case class EnvironmentDefinition(
)
}
+ def withTransferCommandSupport: EnvironmentDefinition =
+ this.addConfigTransform((_, conf) =>
+ ConfigTransforms.updateAllValidatorAppConfigs_(
+ _.copy(enableDeprecatedTransferCommandSupport = true)
+ )(conf)
+ )
+
def clearConfigTransforms(): EnvironmentDefinition =
copy(configTransformsWithContext = _ => Seq())
diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternalPartySetupProposalIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternalPartySetupProposalIntegrationTest.scala
index a6b4b8fab9..6ab887197c 100644
--- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternalPartySetupProposalIntegrationTest.scala
+++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternalPartySetupProposalIntegrationTest.scala
@@ -103,6 +103,7 @@ class ExternalPartySetupProposalIntegrationTest
NonNegativeFiniteDuration.ofMillis(500)
)(config),
)
+ .withTransferCommandSupport
}
diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxTest.scala
index 1517b15167..cddcb116a5 100644
--- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxTest.scala
+++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxTest.scala
@@ -21,7 +21,7 @@ trait ExternallySignedTxTest
override def environmentDefinition: SpliceEnvironmentDefinition = {
EnvironmentDefinition.simpleTopology1Sv(this.getClass.getSimpleName)
- }
+ }.withTransferCommandSupport
def prepareAndSubmitTransfer(keyName: String, sender: PartyId, receiver: PartyId)(implicit
env: SpliceTestConsoleEnvironment
diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxsTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxsTimeBasedIntegrationTest.scala
index f279098757..afda9edb61 100644
--- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxsTimeBasedIntegrationTest.scala
+++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ExternallySignedTxsTimeBasedIntegrationTest.scala
@@ -26,6 +26,7 @@ class ExternallySignedTxsTimeBasedIntegrationTest
override def environmentDefinition: SpliceEnvironmentDefinition =
EnvironmentDefinition
.simpleTopology1SvWithSimTime(this.getClass.getSimpleName)
+ .withTransferCommandSupport
"Externally signed transactions can tolerate a preparation/submission skew larger than ledgerTimeRecordTimeTolerance" in {
implicit env =>
diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LsuIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LsuIntegrationTest.scala
index 0026533aa8..5191fc8be3 100644
--- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LsuIntegrationTest.scala
+++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/LsuIntegrationTest.scala
@@ -29,7 +29,6 @@ import org.lfdecentralizedtrust.splice.environment.{
SequencerAdminConnection,
}
import org.lfdecentralizedtrust.splice.http.v0.definitions
-import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryRequest
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{
IntegrationTest,
@@ -204,6 +203,7 @@ class LsuIntegrationTest
.withSvBftSequencerConnectionDisabled()
.withAmuletPrice(walletAmuletPrice)
.withManualStart
+ .withTransferCommandSupport
override def walletAmuletPrice: java.math.BigDecimal = SpliceUtil.damlDecimal(1.0)
@@ -373,14 +373,13 @@ class LsuIntegrationTest
initDso(includeLocal = false)
startAllSync(aliceValidatorBackend, splitwellValidatorBackend)
actAndCheck("Create some transaction history", sv1WalletClient.tap(1337))(
- "Scan transaction history is recorded and wallet balance is updated",
+ "Wallet balance is updated",
_ => {
// buffer to account for domain fee payments
assertInRange(
sv1WalletClient.balance().unlockedQty,
(walletUsdToAmulet(1000), walletUsdToAmulet(2000)),
)
- countTapsFromScan(sv1ScanBackend, walletUsdToAmulet(1337)) shouldBe 1
},
)
@@ -962,16 +961,6 @@ class LsuIntegrationTest
retryProvider,
)
- private def countTapsFromScan(scan: ScanAppBackendReference, tapAmount: BigDecimal) = {
- listTransactionsFromScan(scan).count(
- _.tap.map(a => BigDecimal(a.amuletAmount)).contains(tapAmount)
- )
- }
-
- private def listTransactionsFromScan(scan: ScanAppBackendReference) = {
- scan.listTransactions(None, TransactionHistoryRequest.SortOrder.Asc, 100)
- }
-
private def getSequencerUrlsConfiguredForTheSync(
participantConnection: ParticipantClientReference,
synchronizerAlias: SynchronizerAlias,
diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/MultiHostValidatorOperatorIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/MultiHostValidatorOperatorIntegrationTest.scala
index 163e7a43d4..277c7d057f 100644
--- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/MultiHostValidatorOperatorIntegrationTest.scala
+++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/MultiHostValidatorOperatorIntegrationTest.scala
@@ -1,11 +1,9 @@
package org.lfdecentralizedtrust.splice.integration.tests
import com.digitalasset.canton.topology.transaction.*
-import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryRequest
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTest
import org.lfdecentralizedtrust.splice.util.WalletTestUtil
-import org.lfdecentralizedtrust.splice.store.Limit
import java.nio.file.Files
import java.util.UUID
@@ -212,19 +210,8 @@ class MultiHostValidatorOperatorIntegrationTest extends IntegrationTest with Wal
)(
"The send succeeds despite alice's validator being disconnected and stopped",
_ => {
- // Fees eat up quite a bit
splitwellWalletClient.balance().unlockedQty should be(60)
- // Alice's wallet is stopped, so we confirm the transaction via scan
- sv1ScanBackend
- .listTransactions(
- None,
- TransactionHistoryRequest.SortOrder.Desc,
- Limit.DefaultMaxPageSize,
- )
- .flatMap(_.transfer)
- .filter(tf =>
- tf.description == transferDescription
- ) should not be empty withClue "transfers splitwell to alice"
+ // don't check on alice's wallet as it's stopped. the transaction going through on splitwell is enough signal.
},
)
}
diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RecoverExternalPartyIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RecoverExternalPartyIntegrationTest.scala
index d08ded7642..fbacabfd1a 100644
--- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RecoverExternalPartyIntegrationTest.scala
+++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RecoverExternalPartyIntegrationTest.scala
@@ -31,7 +31,7 @@ class RecoverExternalPartyIntegrationTest
with WalletTestUtil {
override def environmentDefinition: EnvironmentDefinition =
- EnvironmentDefinition.simpleTopology1Sv(this.getClass.getSimpleName)
+ EnvironmentDefinition.simpleTopology1Sv(this.getClass.getSimpleName).withTransferCommandSupport
override protected lazy val sanityChecksIgnoredRootCreates = Seq(
ValidatorRewardCoupon.TEMPLATE_ID_WITH_PACKAGE_ID
diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RollForwardLsuIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RollForwardLsuIntegrationTest.scala
index 8788c3338d..f1ed840381 100644
--- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RollForwardLsuIntegrationTest.scala
+++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/RollForwardLsuIntegrationTest.scala
@@ -18,7 +18,6 @@ import org.lfdecentralizedtrust.splice.environment.{
MediatorAdminConnection,
SequencerAdminConnection,
}
-import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryRequest
import monocle.macros.syntax.lens.*
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.IntegrationTest
@@ -164,14 +163,13 @@ class RollForwardLsuIntegrationTest
startAllSync(allNodes*)
actAndCheck("Create some transaction history", sv1WalletClient.tap(1337))(
- "Scan transaction history is recorded and wallet balance is updated",
+ "Wallet balance is updated",
_ => {
// buffer to account for domain fee payments
assertInRange(
sv1WalletClient.balance().unlockedQty,
(walletUsdToAmulet(1000), walletUsdToAmulet(2000)),
)
- countTapsFromScan(sv1ScanBackend, walletUsdToAmulet(1337)) shouldBe 1
},
)
@@ -429,16 +427,6 @@ class RollForwardLsuIntegrationTest
retryProvider,
)
- private def countTapsFromScan(scan: ScanAppBackendReference, tapAmount: BigDecimal) = {
- listTransactionsFromScan(scan).count(
- _.tap.map(a => BigDecimal(a.amuletAmount)).contains(tapAmount)
- )
- }
-
- private def listTransactionsFromScan(scan: ScanAppBackendReference) = {
- scan.listTransactions(None, TransactionHistoryRequest.SortOrder.Asc, 100)
- }
-
private def getSequencerUrlSet(
participantConnection: ParticipantClientReference,
synchronizerAlias: SynchronizerAlias,
diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanHistoryBackfillingIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanHistoryBackfillingIntegrationTest.scala
index 7eb0263a7e..619432c55e 100644
--- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanHistoryBackfillingIntegrationTest.scala
+++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanHistoryBackfillingIntegrationTest.scala
@@ -1,6 +1,9 @@
package org.lfdecentralizedtrust.splice.integration.tests
import com.daml.ledger.javaapi.data.Transaction
+import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.*
+import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.actionrequiringconfirmation.*
+import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.dsorules_actionrequiringconfirmation.*
import org.lfdecentralizedtrust.splice.config.ConfigTransforms
import org.lfdecentralizedtrust.splice.config.ConfigTransforms.{
ConfigurableApp,
@@ -20,7 +23,11 @@ import org.lfdecentralizedtrust.splice.scan.automation.{
DeleteCorruptAcsSnapshotTrigger,
ScanHistoryBackfillingTrigger,
}
-import org.lfdecentralizedtrust.splice.store.{PageLimit, TreeUpdateWithMigrationId}
+import org.lfdecentralizedtrust.splice.store.{
+ PageLimit,
+ TreeUpdateWithMigrationId,
+ VoteResultsFilters,
+}
import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.AdvanceOpenMiningRoundTrigger
import org.lfdecentralizedtrust.splice.util.{EventId, UpdateHistoryTestUtil, WalletTestUtil}
import com.digitalasset.canton.config.NonNegativeFiniteDuration
@@ -29,12 +36,12 @@ import com.digitalasset.canton.data.CantonTimestamp
import scala.math.BigDecimal.javaBigDecimal2bigDecimal
import com.digitalasset.canton.{HasActorSystem, HasExecutionContext}
import org.lfdecentralizedtrust.splice.automation.TxLogBackfillingTrigger
-import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryRequest.SortOrder
import org.lfdecentralizedtrust.splice.scan.store.TxLogEntry
import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.TxLogBackfillingState
import org.lfdecentralizedtrust.splice.store.UpdateHistory.BackfillingState
import org.scalactic.source.Position
+import java.util.Optional
import scala.annotation.nowarn
import scala.jdk.CollectionConverters.*
import scala.jdk.OptionConverters.*
@@ -129,6 +136,39 @@ class ScanHistoryBackfillingIntegrationTest
},
)
+ // we create votes as they produce txlog entries that can be backfilled
+ actAndCheck(
+ "Create vote", {
+ val action: ActionRequiringConfirmation =
+ new ARC_DsoRules(
+ new SRARC_SetConfig(
+ new DsoRules_SetConfig(
+ sv1Backend
+ .getDsoInfo()
+ .dsoRules
+ .payload
+ .config,
+ Optional.empty(),
+ )
+ )
+ )
+
+ sv1Backend.createVoteRequest(
+ sv1Backend.getDsoInfo().svParty.toProtoPrimitive,
+ action,
+ "url",
+ "description",
+ sv1Backend.getDsoInfo().dsoRules.payload.config.voteRequestTimeout,
+ None,
+ )
+ },
+ )(
+ "Vote has been executed",
+ _ => {
+ sv1ScanBackend.listVoteRequestResults(VoteResultsFilters(), 100)._1 should have size (1)
+ },
+ )
+
// The current round, as seen by the given scan service (reflects the state of the scan app store)
def currentRoundInScan(backend: ScanAppBackendReference): Long =
backend.getLatestOpenMiningRound(CantonTimestamp.now()).contract.payload.round.number
@@ -466,18 +506,9 @@ class ScanHistoryBackfillingIntegrationTest
.loneElement shouldBe a[TxLogBackfillingTrigger.InitializeBackfillingTask]
}
- clue("TxLog based historical queries differ") {
- val sv1Transactions =
- sv1ScanBackend.listTransactions(None, SortOrder.Asc, 1000).map(shortDebugDescription)
- val sv2Transactions =
- sv2ScanBackend.listTransactions(None, SortOrder.Asc, 1000).map(shortDebugDescription)
-
- // We tapped 4 times before SV2 joined, and once after
- sv1Transactions.size should be >= 5 withClue "SV1 txns"
- sv2Transactions.size should be >= 1 withClue "SV2 txns"
- sv1Transactions.size should be > sv2Transactions.size withClue "SV1 txns"
- sv1Transactions should contain allElementsOf sv2Transactions withClue "sv1 transactions"
- sv2Transactions should not contain sv1Transactions.headOption.value withClue "sv2 transactions"
+ clue("TxLog based vote result result differ") {
+ sv1ScanBackend.listVoteRequestResults(VoteResultsFilters(), 100)._1 should have size (1)
+ sv2ScanBackend.listVoteRequestResults(VoteResultsFilters(), 100)._1 should be(empty)
}
actAndCheck(
@@ -524,14 +555,13 @@ class ScanHistoryBackfillingIntegrationTest
},
)
- clue("TxLog based historical queries return same results") {
- val sv1Transactions =
- sv1ScanBackend.listTransactions(None, SortOrder.Asc, 1000).map(shortDebugDescription)
- val sv2Transactions =
- sv2ScanBackend.listTransactions(None, SortOrder.Asc, 1000).map(shortDebugDescription)
-
- // TODO(#666): switch to theSameElementsInOrderAs once the endpoint sorts by record time instead of row id.
- sv1Transactions should contain theSameElementsAs sv2Transactions withClue "SV1/2 txns"
+ clue("TxLog based vote result queries return same results") {
+ // Not quite sure why we need the eventually given that we sync on backfilling completing but without that sv2ScanBackend can still return an empty list.
+ eventually() {
+ sv1ScanBackend.listVoteRequestResults(VoteResultsFilters(), 100)._1 shouldBe sv2ScanBackend
+ .listVoteRequestResults(VoteResultsFilters(), 100)
+ ._1
+ }
}
}
diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanIntegrationTest.scala
index e478ca2721..147e312ff0 100644
--- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanIntegrationTest.scala
+++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ScanIntegrationTest.scala
@@ -3,9 +3,7 @@ package org.lfdecentralizedtrust.splice.integration.tests
import com.digitalasset.canton.concurrent.Threading
import com.digitalasset.canton.config.NonNegativeFiniteDuration
import com.digitalasset.canton.config.RequireTypes.NonNegativeInt
-import com.digitalasset.canton.data.CantonTimestamp
import com.digitalasset.canton.discard.Implicits.DiscardOps
-import com.digitalasset.canton.topology.PartyId
import org.apache.pekko.http.scaladsl.Http
import org.apache.pekko.http.scaladsl.client.RequestBuilding.{Get, Post}
import org.apache.pekko.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCodes}
@@ -19,10 +17,6 @@ import org.lfdecentralizedtrust.splice.config.ConfigTransforms.{
updateAutomationConfig,
ConfigurableApp,
}
-import org.lfdecentralizedtrust.splice.http.v0.definitions.{
- TransactionHistoryRequest,
- TransactionHistoryResponseItem,
-}
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{
IntegrationTestWithIsolatedEnvironment,
@@ -166,107 +160,6 @@ class ScanIntegrationTest
spliceInstanceNames.nameServiceNameAcronym should be("ANS")
}
- "list transaction pages in ascending and descending order" in { implicit env =>
- val aliceWalletUser = onboardWalletUser(aliceWalletClient, aliceValidatorBackend)
- def tapsForAlice = (t: TransactionHistoryResponseItem) =>
- t.tap.exists { tap =>
- PartyId.tryFromProtoPrimitive(tap.amuletOwner) == aliceWalletUser
- }
-
- val nrTaps = 10
- val amuletAmounts = (1 to nrTaps).map(walletUsdToAmulet(_))
- val pageSize = nrTaps / 2
- // filtering for Alice to avoid interference by the top up taps
- def collectAllTapPagesForAlice(sortOrder: TransactionHistoryRequest.SortOrder) = {
- LazyList
- .iterate(sv1ScanBackend.listTransactions(None, sortOrder, pageSize)) { page =>
- sv1ScanBackend.listTransactions(page.lastOption.map(_.eventId), sortOrder, pageSize)
- }
- .takeWhile(_.nonEmpty)
- .foldLeft(Seq.empty[TransactionHistoryResponseItem])(_ ++ _)
- .filter(tapsForAlice)
- }
-
- def toAmuletAmounts(page: Seq[TransactionHistoryResponseItem]) =
- page.flatMap(_.tap.map(t => BigDecimal(t.amuletAmount)))
-
- actAndCheck(
- "Tap amulets for Alice", {
- (1 to nrTaps).foreach { i =>
- aliceWalletClient.tap(BigDecimal(i))
- }
- },
- )(
- "Amulets should appear in Alice's wallet",
- _ => {
- aliceWalletClient.list().amulets should have length nrTaps.toLong
- },
- )
-
- eventually() {
- val latestRound =
- sv1ScanBackend.getLatestOpenMiningRound(CantonTimestamp.now()).contract.payload.round.number
- val asc = TransactionHistoryRequest.SortOrder.Asc
- val desc = TransactionHistoryRequest.SortOrder.Desc
- val allPagesAsc = collectAllTapPagesForAlice(asc)
- val allPagesDesc = collectAllTapPagesForAlice(desc)
- allPagesAsc.map(_.round) should contain only Some(
- latestRound
- ) withClue "alice tap pages' rounds"
-
- val tapsFirstPageAscending = allPagesAsc.take(pageSize)
-
- toAmuletAmounts(tapsFirstPageAscending) should be(
- amuletAmounts.take(pageSize)
- )
-
- val firstPageEndEventId = tapsFirstPageAscending.last.eventId
- val tapsSecondPageAscending = allPagesAsc.slice(pageSize, pageSize + pageSize)
- sv1ScanBackend
- .listTransactions(
- Some(firstPageEndEventId),
- TransactionHistoryRequest.SortOrder.Asc,
- pageSize.toInt,
- )
- .filter(tapsForAlice)
-
- toAmuletAmounts(tapsSecondPageAscending) should be(
- amuletAmounts.slice(pageSize, pageSize + pageSize)
- )
-
- sv1ScanBackend
- .listTransactions(
- Some(tapsSecondPageAscending.last.eventId),
- asc,
- pageSize.toInt,
- )
- .filter(tapsForAlice) should be(empty)
-
- val tapsFirstPageDescending = allPagesDesc.take(pageSize)
- toAmuletAmounts(tapsFirstPageDescending) should be(
- amuletAmounts.reverse.take(pageSize)
- )
-
- val tapsSecondPageDescending =
- allPagesDesc.slice(pageSize, pageSize + pageSize)
-
- sv1ScanBackend
- .listTransactions(
- Some(tapsSecondPageDescending.last.eventId),
- TransactionHistoryRequest.SortOrder.Desc,
- pageSize.toInt,
- )
- .filter(tapsForAlice) should be(empty)
-
- toAmuletAmounts(tapsSecondPageDescending) should be(
- amuletAmounts.reverse.slice(pageSize, pageSize + pageSize)
- )
- toAmuletAmounts(
- tapsFirstPageAscending ++ tapsSecondPageAscending
- ) should be(toAmuletAmounts((tapsFirstPageDescending ++ tapsSecondPageDescending).reverse))
- }
- }
-
"getUpdateHistory should return 400 for invalid after timestamp" in { implicit env =>
import env.{actorSystem, executionContext}
registerHttpConnectionPoolsCleanup(env)
diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvOnboardingAddlIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvOnboardingAddlIntegrationTest.scala
index cfccc75f9c..25be93015f 100644
--- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvOnboardingAddlIntegrationTest.scala
+++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvOnboardingAddlIntegrationTest.scala
@@ -319,8 +319,7 @@ class SvOnboardingAddlIntegrationTest
forAll(lines)(line => line.message should include("Unexpected amulet create event"))
// Error emitted by every ScanTxLogParser plus the one UserWalletTxLogParser
// associated with the owner of the coin.
- lines should have size 2 withClue "ScanTxLogParser + UserWalletTxLogParser error"
- forExactly(1, lines)(line => line.loggerName should include("sv1Scan"))
+ lines should have size 1 withClue "UserWalletTxLogParser error"
forExactly(1, lines)(line => line.loggerName should include("sv1Validator"))
},
)
diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedRewardCouponIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedRewardCouponIntegrationTest.scala
index 71d16928a4..170b08b3fc 100644
--- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedRewardCouponIntegrationTest.scala
+++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvTimeBasedRewardCouponIntegrationTest.scala
@@ -6,7 +6,6 @@ import org.lfdecentralizedtrust.splice.config.ConfigTransforms.{
ConfigurableApp,
updateAutomationConfig,
}
-import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryRequest
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{
IntegrationTest,
@@ -221,38 +220,7 @@ class SvTimeBasedRewardCouponIntegrationTest
)
}
- clue("The claims appear in the scan history") {
- eventually() {
- val txs = sv1ScanBackend
- .listTransactions(
- None,
- TransactionHistoryRequest.SortOrder.Desc,
- Limit.DefaultMaxPageSize,
- )
- .flatMap(_.transfer)
- .filter(tf =>
- tf.sender.inputSvRewardAmount.nonEmpty &&
- Seq(sv1Party.toProtoPrimitive, aliceValidatorParty.toProtoPrimitive)
- .contains(tf.sender.party)
- )
- .map(tf => tf.sender.party -> tf.sender.inputSvRewardAmount.value)
- .toMap
- BigDecimal(txs(sv1Party.toProtoPrimitive)) should beWithin(
- // The expected SV reward calculated here does not match exactly the reward calculated in daml,
- // presumably because of rounding differences in the reward calculation.
- BigDecimal(eachSvGetInRound0) - 0.001,
- BigDecimal(eachSvGetInRound0) + 0.001,
- )
- BigDecimal(txs(aliceValidatorParty.toProtoPrimitive)) should beWithin(
- // The expected SV reward calculated here does not match exactly the reward calculated in daml,
- // presumably because of rounding differences in the reward calculation.
- BigDecimal(expectedAliceAmount) - 0.001,
- BigDecimal(expectedAliceAmount) + 0.001,
- )
- }
- }
-
- clue("The claims appear in the wallet history") {
+ clue("The claims appear in the SV wallet history") {
eventually() {
val txs = withoutDevNetTopups(
sv1WalletClient
diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogIntegrationTest.scala
index f88e98f41e..f7b02ba426 100644
--- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogIntegrationTest.scala
+++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/WalletTxLogIntegrationTest.scala
@@ -1388,10 +1388,10 @@ class WalletTxLogIntegrationTest
logs =>
inside(logs) {
case logLines if logLines.nonEmpty =>
- logLines
- .filter(_.errorMessage contains ("RuntimeException"))
- .foreach(_.errorMessage should include("Unexpected amulet create event"))
- logLines should have size (env.scans.local.size.toLong + 1) // + 1 for UserWalletTxLog
+ forExactly(1, logLines) { line =>
+ line.errorMessage should include("Unexpected amulet create event")
+ line.loggerName should include("DbMultiDomainAcsStore")
+ }
},
)
@@ -1407,10 +1407,10 @@ class WalletTxLogIntegrationTest
logs =>
inside(logs) {
case logLines if logLines.nonEmpty =>
- logLines
- .filter(_.errorMessage contains ("RuntimeException"))
- .foreach(_.errorMessage should include("Unexpected amulet archive event"))
- logLines should have size (env.scans.local.size.toLong + 1) // + 1 for UserWalletTxLog
+ forExactly(1, logLines) { line =>
+ line.errorMessage should include("Unexpected amulet archive event")
+ line.loggerName should include("DbMultiDomainAcsStore")
+ }
},
)
diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/UpdateHistoryTestUtil.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/UpdateHistoryTestUtil.scala
index db3215507a..cd1d00c8af 100644
--- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/UpdateHistoryTestUtil.scala
+++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/UpdateHistoryTestUtil.scala
@@ -45,7 +45,6 @@ import com.digitalasset.canton.config.RequireTypes.PositiveInt
import com.digitalasset.canton.console.LocalInstanceReference
import com.digitalasset.canton.metrics.MetricValue
import com.digitalasset.canton.topology.{PartyId, SynchronizerId}
-import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryResponseItem
import org.scalatest.Assertion
import scala.jdk.CollectionConverters.*
@@ -443,22 +442,6 @@ trait UpdateHistoryTestUtil extends TestCommon {
def shortDebugDescription(u: Seq[definitions.UpdateHistoryItem]): String = {
u.map(shortDebugDescription).mkString("[\n", ",\n", "\n]")
}
- def shortDebugDescription(u: TransactionHistoryResponseItem): String = {
- // Minimal, human-readable description.
- // Only contains data that is consistent across SVs (in particular, no offset).
- u.transactionType match {
- case TransactionHistoryResponseItem.TransactionType.members.Transfer =>
- s"Transfer(${u.date}, ${u.transfer.value.sender}, ${u.transfer.value.receivers
- .map(r => s"${r.party} -> ${r.amount}")
- .mkString(", ")})"
- case TransactionHistoryResponseItem.TransactionType.members.Mint =>
- s"Mint(${u.date}, ${u.mint.value.amuletOwner}, ${u.mint.value.amuletAmount})"
- case TransactionHistoryResponseItem.TransactionType.members.DevnetTap =>
- s"DevnetTap(${u.date}, ${u.tap.value.amuletOwner}, ${u.tap.value.amuletAmount})"
- case TransactionHistoryResponseItem.TransactionType.members.AbortTransferInstruction =>
- s"AbortTransferInstruction(${u.date}, ${u.abortTransferInstruction.value.transferInstructionCid})"
- }
- }
def dropTrailingNones(u: UpdateHistoryResponse): UpdateHistoryResponse =
u.copy(update = dropTrailingNones(u.update))
diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpClient.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpClient.scala
index c8eff10a11..a09cfb7fa5 100644
--- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpClient.scala
+++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/http/HttpClient.scala
@@ -17,6 +17,8 @@ import org.apache.pekko.http.scaladsl.model.{
HttpHeader,
HttpRequest,
HttpResponse,
+ MediaType,
+ MediaTypes,
StatusCode,
StatusCodes,
}
@@ -50,6 +52,37 @@ trait HttpClient {
}
object HttpClient {
+ private object ResponseErrorByStatus {
+ def unapply(resp: HttpResponse): Option[StatusCode] =
+ resp.status match {
+ case code @ (StatusCodes.ServerError(_) | StatusCodes.ClientError(_)) => Some(code)
+ case _ => None
+ }
+ }
+
+ private object ResponseErrorByContentType {
+ private val validContentTypes: Set[MediaType] = Set(
+ MediaTypes.`application/json`,
+ MediaTypes.`application/octet-stream`,
+ MediaTypes.`text/plain`,
+ )
+
+ def unapply(resp: HttpResponse): Boolean =
+ resp.entity.contentType match {
+ // Responses with `NoContentType` are always considered valid
+ case ContentTypes.NoContentType => false
+ // Otherwise a response is valid if its content type is contained in `validContentTypes`
+ case contentType => !validContentTypes.contains(contentType.mediaType)
+ }
+ }
+
+ private def httpFnErrors(
+ nonErrorStatusCode: Set[StatusCode]
+ ): PartialFunction[HttpResponse, Unit] = {
+ case ResponseErrorByStatus(code) if !nonErrorStatusCode.contains(code) =>
+ case ResponseErrorByContentType() =>
+ }
+
def createHttpFn(
clientName: String,
operationName: String,
@@ -61,10 +94,7 @@ object HttpClient {
): HttpRequest => Future[HttpResponse] = {
httpClientWithErrors(
httpClient.executeRequest(clientName, operationName),
- {
- case code @ (StatusCodes.ServerError(_) | StatusCodes.ClientError(_))
- if !nonErrorStatusCode.contains(code) =>
- },
+ httpFnErrors(nonErrorStatusCode),
)
}
@@ -91,7 +121,7 @@ object HttpClient {
private def httpClientWithErrors(
nextClient: HttpRequest => Future[HttpResponse],
- errors: PartialFunction[StatusCode, Unit],
+ errors: PartialFunction[HttpResponse, Unit],
)(
req: HttpRequest
)(implicit ec: ExecutionContext, mat: Materializer) = {
@@ -102,7 +132,7 @@ object HttpClient {
Future.failed[HttpResponse](error)
}
)
- .applyOrElse(_resp.status, (_: StatusCode) => Future.successful(_resp))
+ .applyOrElse(_resp, Future.successful(_: HttpResponse))
}
}
diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/http/InvalidResponseContentTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/http/InvalidResponseContentTest.scala
new file mode 100644
index 0000000000..2de16000da
--- /dev/null
+++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/http/InvalidResponseContentTest.scala
@@ -0,0 +1,89 @@
+// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package org.lfdecentralizedtrust.splice.http
+
+import cats.data.EitherT
+import com.digitalasset.canton.{BaseTest, HasActorSystem, HasExecutionContext}
+import com.digitalasset.canton.config.NonNegativeDuration
+import io.circe.syntax.*
+import org.lfdecentralizedtrust.splice.auth.AuthToken
+import org.lfdecentralizedtrust.splice.config.AuthTokenSourceConfig
+import org.lfdecentralizedtrust.splice.http.v0.definitions.Version
+import org.apache.pekko.http.scaladsl.model.*
+import org.apache.pekko.stream.Materializer
+import org.lfdecentralizedtrust.splice.http.v0.external.common_admin.{
+ CommonAdminClient,
+ GetVersionResponse,
+ IsReadyResponse,
+}
+import org.scalatest.compatible.Assertion
+import org.scalatest.wordspec.AnyWordSpec
+
+import java.time.{OffsetDateTime, ZoneOffset}
+import scala.concurrent.Future
+import scala.concurrent.duration.Duration
+
+class InvalidResponseContentTest
+ extends AnyWordSpec
+ with BaseTest
+ with HasActorSystem
+ with HasExecutionContext {
+
+ private implicit val mat: Materializer = Materializer(actorSystem)
+
+ private def runEndpoint[A](
+ endpoint: CommonAdminClient => EitherT[Future, Either[Throwable, HttpResponse], A],
+ resp: ResponseEntity,
+ )(
+ expectation: Either[Either[Throwable, HttpResponse], A] => Assertion
+ ): Assertion = {
+ implicit val httpClient: HttpClient = new HttpClient {
+ val requestParameters = HttpClient.HttpRequestParameters(NonNegativeDuration(Duration.Zero))
+ def withOverrideParameters(newParameters: HttpClient.HttpRequestParameters): HttpClient = this
+ def executeRequest(client: String, operation: String)(
+ request: HttpRequest
+ ): Future[HttpResponse] = Future.successful(HttpResponse(StatusCodes.OK, entity = resp))
+ def getToken(authConfig: AuthTokenSourceConfig): Future[Option[AuthToken]] =
+ Future.successful(None)
+ }
+
+ val client = CommonAdminClient.httpClient(HttpClient.createHttpFn("", ""), "http://localhost")
+ expectation(endpoint(client).value.futureValue)
+ }
+
+ "CommonAdminClient.getVersion" should {
+ "include the response body in the error when the content type is invalid" in {
+ val testBody = "
test"
+ runEndpoint(_.getVersion(), HttpEntity(ContentTypes.`text/html(UTF-8)`, testBody)) {
+ case Left(Left(err)) => err.getMessage should endWith(testBody)
+ case other => fail(s"expected Left(Left(throwable)), got: $other")
+ }
+ }
+
+ "decode a well-formed application/json response" in {
+ runEndpoint(
+ _.getVersion(),
+ HttpEntity(
+ ContentTypes.`application/json`,
+ Version(
+ "1.2.3",
+ OffsetDateTime.of(2026, 7, 23, 0, 0, 0, 0, ZoneOffset.UTC),
+ ).asJson.noSpaces,
+ ),
+ ) {
+ case Right(_: GetVersionResponse.OK) => succeed
+ case other => fail(s"expected GetVersionResponse.OK, got: $other")
+ }
+ }
+ }
+
+ "CommonAdminClient.isReady" should {
+ "handle an empty response with no content type" in {
+ runEndpoint(_.isReady(), HttpEntity.Empty) {
+ case Right(IsReadyResponse.OK) => succeed
+ case other => fail(s"expected IsReadyResponse.OK, got: $other")
+ }
+ }
+ }
+}
diff --git a/apps/scan/src/main/openapi/scan.yaml b/apps/scan/src/main/openapi/scan.yaml
index 527fe5fceb..2087189444 100644
--- a/apps/scan/src/main/openapi/scan.yaml
+++ b/apps/scan/src/main/openapi/scan.yaml
@@ -1448,36 +1448,6 @@ paths:
"404":
$ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/404"
- /v0/transactions:
- post:
- deprecated: true
- tags: [deprecated]
- x-jvm-package: scan
- operationId: "listTransactionHistory"
- description: |
- **Deprecated with known bugs that will not be fixed, use /v2/updates instead**.
-
- Lists transactions, by default in ascending order, paged, from ledger begin or optionally starting after a provided event id.
- requestBody:
- required: true
- content:
- application/json:
- schema:
- "$ref": "#/components/schemas/TransactionHistoryRequest"
- responses:
- "200":
- description: ok
- content:
- application/json:
- schema:
- $ref: "#/components/schemas/TransactionHistoryResponse"
- "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/updates:
post:
deprecated: true
@@ -2205,97 +2175,6 @@ components:
svName:
description: The sequencer's operating SV name.
type: string
- TransactionHistoryRequest:
- type: object
- required:
- - page_size
- properties:
- page_end_event_id:
- type: string
- description: |
- Note that all transactions carry some monotonically-increasing event_id.
- Omit this page_end_event_id to start reading the first page, from the beginning or the end of the ledger, depending on the sort_order column.
- A subsequent request can fill the page_end_event_id with the last event_id of the TransactionHistoryResponse to continue reading in the same sort_order.
- The transaction with event_id == page_end_event_id will be skipped in the next response, making it possible to continuously read pages in the same sort_order.
- sort_order:
- description: |
- Sort order for the transactions. For ascending order, from beginning to the end of the ledger, use "asc".
- For descending order, from end to beginning of the ledger, use "desc".
- "asc" is used if the sort_order is omitted.
- type: string
- enum:
- - "asc"
- - "desc"
- page_size:
- description: |
- The maximum number of transactions returned for this request.
- type: integer
- format: int64
- TransactionHistoryResponse:
- type: object
- required:
- - transactions
- properties:
- transactions:
- type: array
- items:
- $ref: "#/components/schemas/TransactionHistoryResponseItem"
- TransactionHistoryResponseItem:
- type: object
- required:
- - transaction_type
- - event_id
- - date
- - domain_id
- properties:
- transaction_type:
- description: |
- Describes the type of activity that occurred.
- Determines if the data for the transaction should be read
- from the `transfer`, `mint`, or `tap` property.
- type: string
- enum:
- - "transfer"
- - "mint"
- - "devnet_tap"
- - "abort_transfer_instruction"
- event_id:
- description: |
- The event id.
- type: string
- offset:
- description: |
- The ledger offset of the event.
- Note that this field may not be the same across nodes, and therefore should not be compared between SVs.
- type: string
- date:
- description: |
- The effective date of the event.
- type: string
- format: date-time
- domain_id:
- description: |
- The id of the domain through which this transaction was sequenced.
- type: string
- round:
- description: |
- The round for which this transaction was registered.
- type: integer
- format: int64
- transfer:
- description: |
- A (batch) transfer from sender to receivers.
- $ref: "#/components/schemas/Transfer"
- mint:
- description: |
- The DSO mints amulet for the cases where the DSO rules allow for that.
- $ref: "#/components/schemas/AmuletAmount"
- tap:
- description: |
- A tap creates a Amulet, only used for development purposes, and enabled only on DevNet.
- $ref: "#/components/schemas/AmuletAmount"
- abort_transfer_instruction:
- $ref: "#/components/schemas/AbortTransferInstruction"
UpdateHistoryRequestAfter:
type: object
required:
diff --git a/apps/scan/src/main/protobuf/scan_tx_log.proto b/apps/scan/src/main/protobuf/scan_tx_log.proto
index 167d0b1361..34b3215631 100644
--- a/apps/scan/src/main/protobuf/scan_tx_log.proto
+++ b/apps/scan/src/main/protobuf/scan_tx_log.proto
@@ -9,13 +9,6 @@ import "google/protobuf/timestamp.proto";
import "google/protobuf/struct.proto";
import "scalapb/scalapb.proto";
-message PartyBalanceChange {
- option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
-
- string change_to_initial_amount_as_of_round_zero = 1 [(scalapb.field).type = "scala.math.BigDecimal"];
- string change_to_holding_fees_rate = 2 [(scalapb.field).type = "scala.math.BigDecimal"];
-}
-
message SteppedRate {
option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
@@ -30,38 +23,6 @@ message SteppedRate {
repeated Step steps = 2;
}
-message SenderAmount {
- option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
-
- string party = 1 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"];
- string input_amulet_amount = 2 [(scalapb.field).type = "scala.math.BigDecimal"];
- string input_app_reward_amount = 3 [(scalapb.field).type = "scala.math.BigDecimal"];
- string input_validator_reward_amount = 4 [(scalapb.field).type = "scala.math.BigDecimal"];
- string sender_change_amount = 5 [(scalapb.field).type = "scala.math.BigDecimal"];
- string sender_change_fee = 6 [(scalapb.field).type = "scala.math.BigDecimal"];
- string sender_fee = 7 [(scalapb.field).type = "scala.math.BigDecimal"];
- string holding_fees = 8 [(scalapb.field).type = "scala.math.BigDecimal"];
- // Added after initial release, so needs to be mapped to an Option in scala
- string input_sv_reward_amount = 9 [(scalapb.field).type = "Option[scala.math.BigDecimal]"];
- string input_validator_faucet_amount = 10 [(scalapb.field).type = "Option[scala.math.BigDecimal]"];
-}
-
-message ReceiverAmount {
- option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
-
- string party = 1 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"];
- string amount = 2 [(scalapb.field).type = "scala.math.BigDecimal"];
- string receiver_fee = 3 [(scalapb.field).type = "scala.math.BigDecimal"];
-}
-
-message BalanceChange {
- option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
-
- string party = 1 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"];
- string change_to_initial_amount_as_of_round_zero = 2 [(scalapb.field).type = "scala.math.BigDecimal"];
- string change_to_holding_fees_rate = 3 [(scalapb.field).type = "scala.math.BigDecimal"];
-}
-
message ErrorTxLogEntry {
option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry";
option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
@@ -69,30 +30,6 @@ message ErrorTxLogEntry {
string event_id = 1;
}
-message BalanceChangeTxLogEntry {
- option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry";
- option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
-
- string event_id = 1;
- string domain_id = 2 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"];
- int64 round = 3;
- string change_to_initial_amount_as_of_round_zero = 4 [(scalapb.field).type = "scala.math.BigDecimal"];
- string change_to_holding_fees_rate = 5 [(scalapb.field).type = "scala.math.BigDecimal"];
- map party_balance_changes = 6 [(scalapb.field).key_type = "com.digitalasset.canton.topology.PartyId"];;
-}
-
-message ExtraTrafficPurchaseTxLogEntry {
- option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry";
- option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
-
- string event_id = 1;
- string domain_id = 2 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"];
- int64 round = 3;
- string validator = 4 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"];
- int64 traffic_purchased = 5;
- string cc_spent = 6 [(scalapb.field).type = "scala.math.BigDecimal"];
-}
-
message OpenMiningRoundTxLogEntry {
option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry";
option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
@@ -116,98 +53,6 @@ message ClosedMiningRoundTxLogEntry {
google.protobuf.Timestamp effective_at = 4 [(scalapb.field).type = "java.time.Instant"];
}
-enum TransferKind {
- TRANSFER_KIND_OTHER = 0;
- TRANSFER_KIND_CREATE_TRANSFER_INSTRUCTION = 1;
- TRANSFER_KIND_TRANSFER_INSTRUCTION_ACCEPT = 2;
- TRANSFER_KIND_PREAPPROVAL_SEND = 3;
-}
-
-message TransferTxLogEntry {
- option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.TransactionTxLogEntry";
- option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
-
- string event_id = 1;
- string offset = 2;
- string domain_id = 3 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"];
- google.protobuf.Timestamp date = 4 [(scalapb.field).type = "java.time.Instant"];
- string provider = 5; // unused: reserved fields don't work well with the json en/decoding so we keep it here.
- SenderAmount sender = 6;
- repeated ReceiverAmount receivers = 7;
- repeated BalanceChange balance_changes = 8;
- int64 round = 9;
- string amulet_price = 10; // Unused but our decoding infrastructure doesn't like reserved fields.
-
- string description = 11;
-
- string transfer_instruction_receiver = 12;
- string transfer_instruction_amount = 13 [(scalapb.field).type = "Option[scala.math.BigDecimal]"];
- string transfer_instruction_cid = 14;
-
- TransferKind transfer_kind = 15;
-}
-
-message TapTxLogEntry {
- option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.TransactionTxLogEntry";
- option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
-
- string event_id = 1;
- string offset = 2;
- string domain_id = 3 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"];
- google.protobuf.Timestamp date = 4 [(scalapb.field).type = "java.time.Instant"];
- string amulet_owner = 5 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"];
- string amulet_amount = 6 [(scalapb.field).type = "scala.math.BigDecimal"];
- int64 round = 7;
- string amulet_price = 8; // Unused but our decoding infrastructure doesn't like reserved fields.
-}
-
-message MintTxLogEntry {
- option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.TransactionTxLogEntry";
- option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
-
- string event_id = 1;
- string offset = 2;
- string domain_id = 3 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"];
- google.protobuf.Timestamp date = 4 [(scalapb.field).type = "java.time.Instant"];
- string amulet_owner = 5 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"];
- string amulet_amount = 6 [(scalapb.field).type = "scala.math.BigDecimal"];
- int64 round = 7;
- string amulet_price = 8; // Unused but our decoding infrastructure doesn't like reserved fields.
-}
-
-message SvRewardTxLogEntry {
- option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.RewardTxLogEntry";
- option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
-
- string event_id = 1;
- string domain_id = 2 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"];
- int64 round = 3;
- string party = 4 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"];
- string amount = 5 [(scalapb.field).type = "scala.math.BigDecimal"];
-}
-
-message AppRewardTxLogEntry {
- option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.RewardTxLogEntry";
- option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
-
- string event_id = 1;
- string domain_id = 2 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"];
- int64 round = 3;
- string party = 4 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"];
- string amount = 5 [(scalapb.field).type = "scala.math.BigDecimal"];
-}
-
-message ValidatorRewardTxLogEntry {
- option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.RewardTxLogEntry";
- option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
-
- string event_id = 1;
- string domain_id = 2 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"];
- int64 round = 3;
- string party = 4 [(scalapb.field).type = "com.digitalasset.canton.topology.PartyId"];
- string amount = 5 [(scalapb.field).type = "scala.math.BigDecimal"];
-}
-
message VoteRequestTxLogEntry {
option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry";
option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
@@ -253,17 +98,3 @@ enum TransferAbortKind {
TRANSFER_ABORT_KIND_WITHDRAW = 1;
TRANSFER_ABORT_KIND_REJECT = 2;
}
-
-message AbortTransferInstructionTxLogEntry {
- option (scalapb.message).extends = "org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.TransactionTxLogEntry";
- option (scalapb.message).companion_extends = "org.lfdecentralizedtrust.splice.store.TxLogStore.TxLogEntryTypeMappers";
-
- string event_id = 1;
- string offset = 2;
- string domain_id = 3 [(scalapb.field).type = "com.digitalasset.canton.topology.SynchronizerId"];
- google.protobuf.Timestamp date = 4 [(scalapb.field).type = "java.time.Instant"];
-
- string transfer_instruction_cid = 14;
-
- TransferAbortKind transfer_abort_kind = 15;
-}
diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/commands/HttpScanAppClient.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/commands/HttpScanAppClient.scala
index 28a6bf83d0..e8211bcc08 100644
--- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/commands/HttpScanAppClient.scala
+++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/api/client/commands/HttpScanAppClient.scala
@@ -894,33 +894,6 @@ object HttpScanAppClient {
final case class DsoScan(publicUrl: Uri, svName: String)
- case class ListTransactions(
- pageEndEventId: Option[String],
- sortOrder: definitions.TransactionHistoryRequest.SortOrder,
- pageSize: Int,
- ) extends InternalBaseCommand[http.ListTransactionHistoryResponse, Seq[
- definitions.TransactionHistoryResponseItem
- ]] {
- override def submitRequest(
- client: http.ScanClient,
- headers: List[HttpHeader],
- ): EitherT[Future, Either[
- Throwable,
- HttpResponse,
- ], http.ListTransactionHistoryResponse] = {
- client.listTransactionHistory(
- definitions
- .TransactionHistoryRequest(pageEndEventId, Some(sortOrder), pageSize.toLong),
- headers,
- )
- }
-
- override def handleOk()(implicit decoder: TemplateJsonDecoder) = {
- case http.ListTransactionHistoryResponse.OK(response) =>
- Right(response.transactions)
- }
- }
-
case class GetAcsSnapshot(
party: PartyId,
recordTime: Option[Instant],
diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala
index 20767f7b21..14e2066ec2 100644
--- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala
+++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/admin/http/HttpScanHandler.scala
@@ -107,7 +107,6 @@ import org.lfdecentralizedtrust.splice.store.{
AppStore,
AppStoreWithIngestion,
PageLimit,
- SortOrder,
VoteResultsFilters,
VotesStore,
}
@@ -694,33 +693,6 @@ class HttpScanHandler(
}
}
- override def listTransactionHistory(
- respond: v0.ScanResource.ListTransactionHistoryResponse.type
- )(
- request: definitions.TransactionHistoryRequest
- )(extracted: TraceContext): Future[v0.ScanResource.ListTransactionHistoryResponse] = {
- implicit val tc = extracted
- withSpan(s"$workflowId.listTransactions") { _ => _ =>
- val pageEndEventId =
- if (request.pageEndEventId.exists(_.isEmpty)) None else request.pageEndEventId
- val sortOrder = request.sortOrder
- .fold[SortOrder](SortOrder.Ascending) {
- case definitions.TransactionHistoryRequest.SortOrder.members.Asc => SortOrder.Ascending
- case definitions.TransactionHistoryRequest.SortOrder.members.Desc => SortOrder.Descending
- }
-
- for {
- txs <- store.listTransactions(
- pageEndEventId,
- sortOrder,
- PageLimit.tryCreate(request.pageSize.intValue()),
- )
- } yield definitions.TransactionHistoryResponse(
- txs.map(TxLogEntry.Http.toResponseItem).toVector
- )
- }
- }
-
def getUpdateHistory(
after: Option[definitions.UpdateHistoryRequestAfter] = None,
pageSize: Int,
diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanStore.scala
index c3f054b2cc..dec98aef00 100644
--- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanStore.scala
+++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/CachingScanStore.scala
@@ -35,9 +35,7 @@ import org.lfdecentralizedtrust.splice.store.{
Limit,
MiningRoundsStore,
MultiDomainAcsStore,
- PageLimit,
ResultsPage,
- SortOrder,
SynchronizerStore,
TxLogStore,
UpdateHistory,
@@ -192,17 +190,6 @@ class CachingScanStore(
store.lookupTransferCommandCounterByParty,
).get(partyId)
- override def listTransactions(
- pageEndEventId: Option[String],
- sortOrder: SortOrder,
- limit: PageLimit,
- )(implicit tc: TraceContext): Future[Seq[TxLogEntry.TransactionTxLogEntry]] =
- store.listTransactions(
- pageEndEventId,
- sortOrder,
- limit,
- )
-
override def lookupLatestTransferCommandEvents(sender: PartyId, nonce: Long, limit: Int)(implicit
tc: TraceContext
): Future[Map[TransferCommand.ContractId, TransferCommandTxLogEntry]] =
diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanStore.scala
index fbe1500723..60c9f7e649 100644
--- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanStore.scala
+++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanStore.scala
@@ -30,8 +30,6 @@ import org.lfdecentralizedtrust.splice.store.{
ExternalPartyConfigStateStore,
MiningRoundsStore,
MultiDomainAcsStore,
- PageLimit,
- SortOrder,
TxLogAppStore,
UpdateHistory,
VotesStore,
@@ -215,14 +213,6 @@ trait ScanStore
]]
]
- def listTransactions(
- pageEndEventId: Option[String],
- sortOrder: SortOrder,
- limit: PageLimit,
- )(implicit
- tc: TraceContext
- ): Future[Seq[TxLogEntry.TransactionTxLogEntry]]
-
def lookupLatestTransferCommandEvents(
sender: PartyId,
nonce: Long,
diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanTxLogParser.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanTxLogParser.scala
index a33c651e98..47a6e1f913 100644
--- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanTxLogParser.scala
+++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/ScanTxLogParser.scala
@@ -9,10 +9,6 @@ import com.daml.ledger.javaapi.data.*
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
import com.digitalasset.canton.topology.{PartyId, SynchronizerId}
import com.digitalasset.canton.tracing.TraceContext
-import io.grpc.Status
-import org.lfdecentralizedtrust.splice.codegen.java.splice
-import org.lfdecentralizedtrust.splice.codegen.java.splice.amulet.AmuletCreateSummary
-import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletrules.TransferResult
import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.{
DsoRules_CloseVoteRequest,
DsoRules_CloseVoteRequestResult,
@@ -21,23 +17,13 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.externalpartyamuletru
TransferCommandResultFailure,
TransferCommandResultSuccess,
}
-import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.subscriptions as sws
import org.lfdecentralizedtrust.splice.history.*
-import org.lfdecentralizedtrust.splice.scan.store.TxLogEntry.*
import org.lfdecentralizedtrust.splice.store.TxLogStore
import org.lfdecentralizedtrust.splice.store.events.DsoRulesCloseVoteRequest
import org.lfdecentralizedtrust.splice.util.SpliceUtil.dollarsToCC
-import org.lfdecentralizedtrust.splice.util.TransactionTreeExtensions.*
-import org.lfdecentralizedtrust.splice.util.{
- Codec,
- EventId,
- ExerciseNode,
- LegacyOffset,
- TokenStandardMetadata,
-}
+import org.lfdecentralizedtrust.splice.util.{Codec, EventId, ExerciseNode}
import scala.collection.immutable
-import scala.jdk.OptionConverters.*
import scala.jdk.CollectionConverters.*
import scala.math.BigDecimal.javaBigDecimal2bigDecimal
@@ -81,387 +67,6 @@ class ScanTxLogParser(
exercised.getNodeId,
)
exercised match {
- case Transfer(node) =>
- State.fromTransfer(tree, exercised, synchronizerId, node)
- case TransferPreapproval_Send(node) =>
- val state = parseTrees(
- tree,
- synchronizerId,
- tree.getChildNodeIds(exercised).asScala.toList,
- ignoreUnexpectedAmuletCreateArchive,
- )
- state.setTransferPreapprovalSendFields(tree, exercised, node.argument.value.description)
- case TransferPreapproval_SendV2(node) =>
- val receiver = (node.result.value.result.summary.balanceChanges.asScala.keySet
- .diff(Set(node.argument.value.sender)))
- .headOption
- .getOrElse(node.argument.value.sender)
- val output = new splice.amuletrules.TransferOutput(
- receiver,
- BigDecimal(0).bigDecimal, // receiver fee ratio is irrelevant, there are no fees
- node.argument.value.amount,
- java.util.Optional.empty(), // lock
- java.util.Optional.empty(), // meta
- )
- val state = State.fromTransferResult(
- tree,
- exercised,
- synchronizerId,
- sender = node.argument.value.sender,
- outputs = Seq(output),
- result = node.result.value.result,
- )
- val description =
- node.result.value.meta.values.asScala.get(TokenStandardMetadata.reasonMetaKey)
- state.setTransferPreapprovalSendFields(tree, exercised, description.toJava)
- case CreateTokenStandardTransferInstruction(node) =>
- // TODO(tech-debt): remove this duplication with CreateTokenStandardTransferInstructionV2
- val cid: String = node.result.value.output match {
- case output: splice.api.token.transferinstructionv1.transferinstructionresult_output.TransferInstructionResult_Pending =>
- output.transferInstructionCid.contractId
- case output =>
- // CreateTokenStandardTransferInstruction only matches on two-step transfers resulting in pending status.
- // Single-step transfers are just parsed as the underlying transfer.
- logger.warn(
- s"Unexpected transfer instruction result output, expected pending but got: $output"
- )
- ""
- }
- val state = parseTrees(
- tree,
- synchronizerId,
- tree.getChildNodeIds(exercised).asScala.toList,
- ignoreUnexpectedAmuletCreateArchive = true,
- )
- val stateWithTransfer = if (state.hasTransfer) {
- // We hit this for transfers before the 24h signing change that call AmuletRules_Transfer internally.
- state
- } else {
- val txLogEntry = new TransferTxLogEntry(
- offset = LegacyOffset.Api.fromLong(tree.getOffset),
- domainId = synchronizerId,
- date = Some(tree.getEffectiveAt),
- sender = Some(
- senderAmountNoFees(
- node.argument.value.transfer.sender,
- 0.0, // Note: Because Scan tracks the sum of locked and unlocked input and output is 0.
- )
- ),
- // receiver is set to the sender as the amulet is locked to them
- receivers = Seq(
- receiverAmountNoFees(
- node.argument.value.transfer.sender,
- node.argument.value.transfer.amount,
- )
- ),
- balanceChanges = Seq.empty,
- )
- State(
- txLogEntry
- )
- }
- stateWithTransfer.copy(
- entries = stateWithTransfer.entries.map {
- case e: TransferTxLogEntry =>
- e.copy(
- description = node.argument.value.transfer.meta.values
- .getOrDefault(TokenStandardMetadata.reasonMetaKey, ""),
- transferInstructionReceiver = node.argument.value.transfer.receiver,
- transferInstructionAmount = Some(node.argument.value.transfer.amount),
- transferInstructionCid = cid,
- eventId =
- EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId),
- transferKind = TransferKind.TRANSFER_KIND_CREATE_TRANSFER_INSTRUCTION,
- )
- case e => e
- }
- )
- case DirectTokenStandardTransfer(node) =>
- // TODO(tech-debt): remove this duplication with DirectTokenStandardTransferV2
- val sender = node.argument.value.transfer.sender
- val receiver = node.argument.value.transfer.receiver
- val amount = node.argument.value.transfer.amount
-
- val senderAmount = senderAmountNoFees(sender, amount)
- val state = parseTrees(
- tree,
- synchronizerId,
- tree.getChildNodeIds(exercised).asScala.toList,
- ignoreUnexpectedAmuletCreateArchive = true,
- )
- if (state.hasTransfer) {
- // We hit this for transfers before the 24h signing delay change that call AmuletRules_Transfer or TransferPreapproval_Send internally
- // or transfers with the 24h signing delay change where sender != receiver which call into TransferPreapproval_SendV2
- state
- } else {
- // We hit this only when sender = receiver and the 24h signing delay change is active as then there is no TransferPreapproval_SendV2 child.
- // We just parse this as a plain transfer matching the behavior before the 24h signing delay change.
- val txLogEntry = new TransferTxLogEntry(
- offset = LegacyOffset.Api.fromLong(tree.getOffset),
- eventId =
- EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId),
- domainId = synchronizerId,
- date = Some(tree.getEffectiveAt),
- sender = Some(senderAmount),
- receivers = Seq(receiverAmountNoFees(receiver, amount)),
- balanceChanges = Seq.empty,
- description = node.argument.value.transfer.meta.values
- .getOrDefault(TokenStandardMetadata.reasonMetaKey, ""),
- )
- State(txLogEntry)
- }
- case CreateTokenStandardTransferInstructionV2(node) =>
- // TODO(tech-debt): remove this duplication with CreateTokenStandardTransferInstruction
- val admin = node.argument.value.transfer.instrumentId.admin
- val cid: String = node.result.value.output match {
- case output: splice.api.token.transferinstructionv2.transferinstructionresult_output.TransferInstructionResult_Pending =>
- output.transferInstructionCid.contractId
- case output =>
- // CreateTokenStandardTransferInstruction only matches on two-step transfers resulting in pending status.
- // Single-step transfers are just parsed as the underlying transfer.
- logger.warn(
- s"Unexpected transfer instruction result output, expected pending but got: $output"
- )
- ""
- }
- val state = parseTrees(
- tree,
- synchronizerId,
- tree.getChildNodeIds(exercised).asScala.toList,
- ignoreUnexpectedAmuletCreateArchive = true,
- )
- val stateWithTransfer = if (state.hasTransfer) {
- // We hit this for transfers before the 24h signing change that call AmuletRules_Transfer internally.
- state
- } else {
- val txLogEntry = new TransferTxLogEntry(
- offset = LegacyOffset.Api.fromLong(tree.getOffset),
- domainId = synchronizerId,
- date = Some(tree.getEffectiveAt),
- sender = Some(
- senderAmountNoFees(
- node.argument.value.transfer.sender.owner.toScala.getOrElse(admin),
- 0.0, // Note: Because Scan tracks the sum of locked and unlocked input and output is 0.
- )
- ),
- // receiver is set to the sender as the amulet is locked to them
- receivers = Seq(
- receiverAmountNoFees(
- node.argument.value.transfer.sender.owner.toScala.getOrElse(admin),
- node.argument.value.transfer.amount,
- )
- ),
- balanceChanges = Seq.empty,
- )
- State(
- txLogEntry
- )
- }
- stateWithTransfer.copy(
- entries = stateWithTransfer.entries.map {
- case e: TransferTxLogEntry =>
- e.copy(
- description = node.argument.value.transfer.meta.values
- .getOrDefault(TokenStandardMetadata.reasonMetaKey, ""),
- transferInstructionReceiver =
- node.argument.value.transfer.receiver.owner.toScala.getOrElse(admin),
- transferInstructionAmount = Some(node.argument.value.transfer.amount),
- transferInstructionCid = cid,
- eventId =
- EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId),
- transferKind = TransferKind.TRANSFER_KIND_CREATE_TRANSFER_INSTRUCTION,
- )
- case e => e
- }
- )
- case DirectTokenStandardTransferV2(node) =>
- // TODO(tech-debt): remove this duplication with DirectTokenStandardTransfer
- val admin = node.argument.value.transfer.instrumentId.admin
- val sender = node.argument.value.transfer.sender.owner.toScala.getOrElse(admin)
- val receiver = node.argument.value.transfer.receiver.owner.toScala.getOrElse(admin)
- val amount = node.argument.value.transfer.amount
-
- val senderAmount = senderAmountNoFees(sender, amount)
- val state = parseTrees(
- tree,
- synchronizerId,
- tree.getChildNodeIds(exercised).asScala.toList,
- ignoreUnexpectedAmuletCreateArchive = true,
- )
- if (state.hasTransfer) {
- // We hit this for transfers before the 24h signing delay change that call AmuletRules_Transfer or TransferPreapproval_Send internally
- // or transfers with the 24h signing delay change where sender != receiver which call into TransferPreapproval_SendV2
- state
- } else {
- // We hit this only when sender = receiver and the 24h signing delay change is active as then there is no TransferPreapproval_SendV2 child.
- // We just parse this as a plain transfer matching the behavior before the 24h signing delay change.
- val txLogEntry = new TransferTxLogEntry(
- offset = LegacyOffset.Api.fromLong(tree.getOffset),
- eventId =
- EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId),
- domainId = synchronizerId,
- date = Some(tree.getEffectiveAt),
- sender = Some(senderAmount),
- receivers = Seq(receiverAmountNoFees(receiver, amount)),
- balanceChanges = Seq.empty,
- description = node.argument.value.transfer.meta.values
- .getOrDefault(TokenStandardMetadata.reasonMetaKey, ""),
- )
- State(txLogEntry)
- }
- case TransferInstruction_Accept(node) =>
- val state = parseTrees(
- tree,
- synchronizerId,
- tree.getChildNodeIds(exercised).asScala.toList,
- ignoreUnexpectedAmuletCreateArchive = true,
- )
- val stateWithTransfer = if (state.hasTransfer) {
- // We hit this for transfers before the 24h signing change that call AmuletRules_Transfer internally.
- state
- } else {
- val coinCid = node.result.value.output match {
- case output: splice.api.token.transferinstructionv1.transferinstructionresult_output.TransferInstructionResult_Completed =>
- assert(output.receiverHoldingCids.size == 1)
- output.receiverHoldingCids.get(0)
- case output =>
- throw new RuntimeException(
- s"Unexpected transfer instruction result output, expected completed but got: $output"
- )
- }
- val coin =
- tree
- .findCreation(
- splice.amulet.Amulet.COMPANION,
- new splice.amulet.Amulet.ContractId(coinCid.contractId),
- )
- .getOrElse(
- throw new RuntimeException(
- s"The amulet contract ${coinCid} was not found in transaction ${tree.getUpdateId}"
- )
- )
- val sender = node.result.value.meta.values.get(TokenStandardMetadata.senderMetaKey)
- val receiver = coin.payload.owner
- val amount = coin.payload.amount.initialAmount
-
- val txLogEntry = new TransferTxLogEntry(
- offset = LegacyOffset.Api.fromLong(tree.getOffset),
- eventId =
- EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId),
- domainId = synchronizerId,
- date = Some(tree.getEffectiveAt),
- sender = Some(senderAmountNoFees(sender, amount)),
- receivers = Seq(receiverAmountNoFees(receiver, amount)),
- balanceChanges = Seq.empty,
- )
- State(txLogEntry)
- }
- stateWithTransfer.copy(
- entries = stateWithTransfer.entries.map {
- case e: TransferTxLogEntry =>
- e.copy(
- transferInstructionCid = exercised.getContractId,
- transferKind = TransferKind.TRANSFER_KIND_TRANSFER_INSTRUCTION_ACCEPT,
- )
- case e => e
- }
- )
- case TransferInstruction_Withdraw(_) =>
- // Contrary to the wallet which tracks only unlocked amulet balance,
- // scan tracks the sum of locked and unlocked balance so
- // this does not actually create a change in balance.
- State(
- AbortTransferInstructionTxLogEntry(
- offset = LegacyOffset.Api.fromLong(tree.getOffset),
- eventId = eventId,
- domainId = synchronizerId,
- date = Some(tree.getEffectiveAt),
- transferInstructionCid = exercised.getContractId,
- transferAbortKind = TransferAbortKind.TRANSFER_ABORT_KIND_WITHDRAW,
- )
- )
- case TransferInstruction_Reject(_) =>
- // Contrary to the wallet which tracks only unlocked amulet balance,
- // scan tracks the sum of locked and unlocked balance so
- // this does not actually create a change in balance.
- State(
- AbortTransferInstructionTxLogEntry(
- offset = LegacyOffset.Api.fromLong(tree.getOffset),
- eventId = eventId,
- domainId = synchronizerId,
- date = Some(tree.getEffectiveAt),
- transferInstructionCid = exercised.getContractId,
- transferAbortKind = TransferAbortKind.TRANSFER_ABORT_KIND_REJECT,
- )
- )
- case Tap(node) =>
- State.fromAmuletCreateSummary(
- tree,
- exercised,
- synchronizerId,
- node.result.value.amuletSum,
- TransactionType.Tap,
- )
- case Mint(node) =>
- State.fromAmuletCreateSummary(
- tree,
- exercised,
- synchronizerId,
- node.result.value.amuletSum,
- TransactionType.Mint,
- )
- case AmuletRules_BuyMemberTraffic(node) =>
- State.fromBuyMemberTraffic(eventId, synchronizerId, node)
- case AmuletRules_CreateExternalPartySetupProposal(node) =>
- State.fromCreateExternalPartySetupProposal(eventId, synchronizerId, node)
- case AmuletRules_CreateTransferPreapproval(node) =>
- State.fromCreateTransferPreapproval(eventId, synchronizerId, node)
- case TransferPreapproval_Renew(node) =>
- State.fromRenewTransferPreapproval(eventId, synchronizerId, node)
- case AmuletExpire(node) =>
- State.empty
- case AmuletExpireV2(node) =>
- State.empty
- case LockedAmuletExpireAmulet(node) =>
- State.empty
- case LockedAmuletExpireAmuletV2(node) =>
- State.empty
- // We track the sum of locked/unlocked so this is a noop.
- case LockedAmuletUnlock(_) =>
- State.empty
- // We track the sum of locked/unlocked so this is a noop.
- case LockedAmuletUnlockV2(_) =>
- State.empty
- // We track the sum of locked/unlocked so this is a noop.
- case LockedAmuletOwnerExpireLock(_) =>
- State.empty
- // We track the sum of locked/unlocked so this is a noop.
- case LockedAmuletOwnerExpireLockV2(_) =>
- State.empty
- case AnsRules_CollectInitialEntryPayment(_) =>
- fromAnsEntryPaymentCollection(
- tree,
- exercised,
- synchronizerId,
- sws.SubscriptionInitialPayment.COMPANION,
- sws.SubscriptionInitialPayment.CHOICE_SubscriptionInitialPayment_Collect,
- )(_.amulet)
- case AnsRules_CollectEntryRenewalPayment(_) =>
- fromAnsEntryPaymentCollection(
- tree,
- exercised,
- synchronizerId,
- sws.SubscriptionPayment.COMPANION,
- sws.SubscriptionPayment.CHOICE_SubscriptionPayment_Collect,
- )(_.amulet)
- case AmuletArchive(_) =>
- if (!ignoreUnexpectedAmuletCreateArchive) {
- throw new RuntimeException(
- s"Unexpected amulet archive event for amulet ${exercised.getContractId} in transaction ${tree.getUpdateId}"
- )
- } else {
- State.empty
- }
case DsoRulesCloseVoteRequest(node) =>
State.fromCloseVoteRequest(eventId, node)
case ExternalPartyAmuletRules_CreateTransferCommand(node) =>
@@ -479,78 +84,6 @@ class ScanTxLogParser(
State.fromTransferCommand_Withdraw(eventId, exercised, node)
case TransferCommand_Expire(node) =>
State.fromTransferCommand_Expire(eventId, exercised, node)
- case AllocationFactoryAllocate(node) =>
- val state = parseTrees(
- tree,
- synchronizerId,
- tree.getChildNodeIds(exercised).asScala.toList,
- ignoreUnexpectedAmuletCreateArchive = true,
- )
- if (state.hasTransfer) {
- // We hit this for allocations before the 24h signing change that call AmuletRules_Transfer internally.
- state
- } else {
-
- val sender = node.argument.value.allocation.transferLeg.sender
- val amount = node.argument.value.allocation.transferLeg.amount
-
- val txLogEntry = new TransferTxLogEntry(
- offset = LegacyOffset.Api.fromLong(tree.getOffset),
- eventId =
- EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId),
- domainId = synchronizerId,
- date = Some(tree.getEffectiveAt),
- sender = Some(senderAmountNoFees(sender, amount)),
- receivers = Seq(
- receiverAmountNoFees(sender, amount)
- ), // This step locks to the sender which scan displays as a transfer to yourself.
- balanceChanges = Seq.empty,
- )
- State(txLogEntry)
- }
- case AllocationExecuteTransfer(node) =>
- val state = parseTrees(
- tree,
- synchronizerId,
- tree.getChildNodeIds(exercised).asScala.toList,
- ignoreUnexpectedAmuletCreateArchive = true,
- )
- if (state.hasTransfer) {
- // We hit this for allocations before the 24h signing change that call AmuletRules_Transfer internally.
- state
- } else {
- assert(node.result.value.receiverHoldingCids.size == 1)
- val coinCid = node.result.value.receiverHoldingCids.get(0)
- val coin =
- tree
- .findCreation(
- splice.amulet.Amulet.COMPANION,
- new splice.amulet.Amulet.ContractId(coinCid.contractId),
- )
- .getOrElse(
- throw new RuntimeException(
- s"The amulet contract ${coinCid} was not found in transaction ${tree.getUpdateId}"
- )
- )
- val sender = node.result.value.meta.values.get(TokenStandardMetadata.senderMetaKey)
- val receiver = coin.payload.owner
- val amount = coin.payload.amount.initialAmount
-
- val txLogEntry = new TransferTxLogEntry(
- offset = LegacyOffset.Api.fromLong(tree.getOffset),
- eventId =
- EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId),
- domainId = synchronizerId,
- date = Some(tree.getEffectiveAt),
- sender = Some(senderAmountNoFees(sender, amount)),
- receivers = Seq(receiverAmountNoFees(receiver, amount)),
- balanceChanges = Seq.empty,
- )
- State(txLogEntry)
- }
- // Token Standard V2: the Scan txlog will go away so we don't bother parsing it
- case AllocationFactoryV2Allocate(_) | AllocationV2Settle(_) =>
- State.empty
case _ =>
parseTrees(
tree,
@@ -573,22 +106,6 @@ class ScanTxLogParser(
)
case ClosedMiningRoundCreate(round) =>
State.fromClosedMiningRoundCreate(tree, root, synchronizerId, round)
- case AmuletCreate(_) =>
- if (!ignoreUnexpectedAmuletCreateArchive) {
- throw new RuntimeException(
- s"Unexpected amulet create event for amulet ${created.getContractId} in transaction ${tree.getUpdateId}"
- )
- } else {
- State.empty
- }
- case LockedAmuletCreate(_) =>
- if (!ignoreUnexpectedAmuletCreateArchive) {
- throw new RuntimeException(
- s"Unexpected locked amulet create event for amulet ${created.getContractId} in transaction ${tree.getUpdateId}"
- )
- } else {
- State.empty
- }
case _ => State.empty
}
@@ -597,35 +114,6 @@ class ScanTxLogParser(
}
}
- private def fromAnsEntryPaymentCollection[Marker, Res](
- tree: Transaction,
- exercised: ExercisedEvent,
- synchronizerId: SynchronizerId,
- paymentCollectionTemplate: codegen.ContractCompanion[?, ?, Marker],
- paymentCollectionChoice: codegen.Choice[Marker, ?, Res],
- )(
- collectionProducedAmulet: Res => AmuletCreate.TCid
- )(implicit tc: TraceContext) = {
- // first child event is the initial subscription payment collected by DSO
- val (paymentCollectionEvent, _) =
- tree
- .firstDescendantExercise(exercised, paymentCollectionTemplate, paymentCollectionChoice)
- .map { case (e, pr) => (e, collectionProducedAmulet(pr)) }
- .getOrElse {
- sys.error(
- s"Unable to find ${paymentCollectionChoice.name} in ${exercised.getChoice}"
- )
- }
-
- val stateFromPaymentCollection = parseTree(
- tree,
- synchronizerId,
- paymentCollectionEvent,
- ignoreUnexpectedAmuletCreateArchive = false,
- )
- State.empty.appended(stateFromPaymentCollection)
- }
-
private def parseTrees(
tree: Transaction,
synchronizerId: SynchronizerId,
@@ -670,29 +158,6 @@ object ScanTxLogParser {
def appended(other: State): State = State(
entries = entries.appendedAll(other.entries)
)
- def hasTransfer: Boolean =
- entries.exists {
- case _: TransferTxLogEntry => true
- case _ => false
- }
-
- def setTransferPreapprovalSendFields(
- tree: Transaction,
- exercised: ExercisedEvent,
- description: java.util.Optional[String],
- ): State =
- copy(
- entries = entries.map {
- case e: TransferTxLogEntry =>
- e.copy(
- description = description.orElse(""),
- eventId =
- EventId.prefixedFromUpdateIdAndNodeId(tree.getUpdateId, exercised.getNodeId),
- transferKind = TransferKind.TRANSFER_KIND_PREAPPROVAL_SEND,
- )
- case e => e
- }
- )
}
private object State {
@@ -709,334 +174,6 @@ object ScanTxLogParser {
a.appended(b)
}
- private def getAmuletFromSummary(
- tx: Transaction,
- ccsum: AmuletCreateSummary[? <: codegen.ContractId[AmuletCreate.T]],
- ) = {
- val amuletCid = ccsum.amulet
- tx.findCreation(AmuletCreate.companion, amuletCid)
- .map(_.payload)
- .getOrElse {
- throw new RuntimeException(
- s"The amulet contract $amuletCid referenced by AmuletCreateSummary was not found in transaction ${tx.getUpdateId}"
- )
- }
- }
-
- def fromAmuletCreateSummary(
- tx: Transaction,
- event: Event,
- synchronizerId: SynchronizerId,
- acsum: AmuletCreateSummary[? <: codegen.ContractId[AmuletCreate.T]],
- activityType: TransactionType,
- ): State = {
- val amulet = getAmuletFromSummary(tx, acsum)
- val eventId = EventId.prefixedFromUpdateIdAndNodeId(tx.getUpdateId, event.getNodeId)
- val activityEntry: TransactionTxLogEntry = activityType match {
- case TransactionType.Tap =>
- TapTxLogEntry(
- offset = LegacyOffset.Api.fromLong(tx.getOffset),
- eventId = eventId,
- domainId = synchronizerId,
- date = Some(tx.getEffectiveAt),
- amuletOwner = PartyId.tryFromProtoPrimitive(amulet.owner),
- amuletAmount = amulet.amount.initialAmount,
- round = acsum.round.number,
- )
- case TransactionType.Mint =>
- MintTxLogEntry(
- offset = LegacyOffset.Api.fromLong(tx.getOffset),
- eventId = eventId,
- domainId = synchronizerId,
- date = Some(tx.getEffectiveAt),
- amuletOwner = PartyId.tryFromProtoPrimitive(amulet.owner),
- amuletAmount = amulet.amount.initialAmount,
- round = acsum.round.number,
- )
- case unexpected =>
- throw new Exception(
- s"unexpected activityType: $unexpected in fromAmuletCreateSummary"
- )
- }
-
- State(activityEntry)
- }
-
- private def rewardsEntriesFromTransferSummary(
- sender: PartyId,
- summary: splice.amuletrules.TransferSummary,
- round: Long,
- synchronizerId: SynchronizerId,
- rootEventId: String,
- ): State = {
- val appRewards = summary.inputAppRewardAmount
- val validatorRewards = summary.inputValidatorRewardAmount
- val svRewards = summary.inputSvRewardAmount
-
- val appRewardEntry =
- if (appRewards.compareTo(BigDecimal(0.0)) > 0) {
- val entry =
- AppRewardTxLogEntry(
- eventId = rootEventId,
- domainId = synchronizerId,
- round = round,
- party = sender,
- amount = appRewards,
- )
- State(entry)
- } else {
- State.empty
- }
-
- val validatorRewardEntry =
- if (validatorRewards.compareTo(BigDecimal(0.0)) > 0) {
- val entry =
- ValidatorRewardTxLogEntry(
- eventId = rootEventId,
- domainId = synchronizerId,
- round = round,
- party = sender,
- amount = validatorRewards,
- )
- State(entry)
- } else {
- State.empty
- }
-
- val svRewardEntry =
- if (svRewards.compareTo(BigDecimal(0.0)) > 0) {
- val entry =
- SvRewardTxLogEntry(
- eventId = rootEventId,
- domainId = synchronizerId,
- round = round,
- party = sender,
- amount = svRewards,
- )
- State(entry)
- } else {
- State.empty
- }
-
- appRewardEntry.appended(validatorRewardEntry).appended(svRewardEntry)
- }
-
- def fromTransfer(
- tx: Transaction,
- event: ExercisedEvent,
- synchronizerId: SynchronizerId,
- node: ExerciseNode[Transfer.Arg, Transfer.Res],
- rootEventId: Option[String] = None,
- ): State = {
- State.fromTransferResult(
- tx,
- event,
- synchronizerId,
- sender = node.argument.value.transfer.sender,
- outputs = node.argument.value.transfer.outputs.asScala.toSeq,
- result = node.result.value,
- rootEventId = rootEventId,
- )
- }
-
- def fromTransferResult(
- tx: Transaction,
- event: ExercisedEvent,
- synchronizerId: SynchronizerId,
- sender: String,
- outputs: Seq[splice.amuletrules.TransferOutput],
- result: splice.amuletrules.TransferResult,
- rootEventId: Option[String] = None,
- ): State = {
- val senderParty = Codec
- .decode(Codec.Party)(sender)
- .getOrElse(
- throw Status.INTERNAL
- .withDescription(s"Cannot decode party ID ${sender}")
- .asRuntimeException()
- )
- val eventId = EventId.prefixedFromUpdateIdAndNodeId(tx.getUpdateId, event.getNodeId)
- val rewardEntries =
- rewardsEntriesFromTransferSummary(
- senderParty,
- result.summary,
- result.round.number,
- synchronizerId,
- rootEventId.getOrElse(eventId),
- )
-
- val activityEntry = State(
- transferTxLogEntry(
- tx,
- event,
- synchronizerId,
- sender = sender,
- outputs = outputs,
- result = result,
- )
- )
-
- rewardEntries
- .appended(activityEntry)
- }
-
- private def transferTxLogEntry(
- tx: Transaction,
- event: Event,
- synchronizerId: SynchronizerId,
- sender: String,
- outputs: Seq[splice.amuletrules.TransferOutput],
- result: splice.amuletrules.TransferResult,
- ): TransferTxLogEntry = {
- val senderAmount = parseSenderAmount(sender, outputs, result)
- val receiverAmounts = parseReceiverAmounts(outputs, result)
-
- new TransferTxLogEntry(
- offset = LegacyOffset.Api.fromLong(tx.getOffset),
- eventId = EventId.prefixedFromUpdateIdAndNodeId(tx.getUpdateId, event.getNodeId),
- domainId = synchronizerId,
- date = Some(tx.getEffectiveAt),
- sender = Some(senderAmount),
- receivers = receiverAmounts,
- round = result.round.number,
- )
- }
-
- def fromBuyMemberTraffic(
- eventId: String,
- synchronizerId: SynchronizerId,
- node: ExerciseNode[AmuletRules_BuyMemberTraffic.Arg, AmuletRules_BuyMemberTraffic.Res],
- ): State = {
- val validatorParty = Codec
- .decode(Codec.Party)(node.argument.value.provider)
- .getOrElse(
- throw Status.INTERNAL
- .withDescription(
- s"Cannot decode party ID ${node.argument.value.provider}"
- )
- .asRuntimeException()
- )
- val round = node.result.value.round
- val trafficPurchased = node.argument.value.trafficAmount
- val ccSpent = node.result.value.amuletPaid
- val buyExtraTrafficEntry = ExtraTrafficPurchaseTxLogEntry(
- eventId = eventId,
- domainId = synchronizerId,
- round = round.number,
- validator = validatorParty,
- trafficPurchased = trafficPurchased,
- ccSpent = ccSpent,
- )
-
- val rewardEntries = rewardsEntriesFromTransferSummary(
- validatorParty,
- node.result.value.summary,
- round.number,
- synchronizerId,
- eventId,
- )
-
- State(buyExtraTrafficEntry)
- .appended(rewardEntries)
- }
-
- def fromCreateExternalPartySetupProposal(
- eventId: String,
- synchronizerId: SynchronizerId,
- node: ExerciseNode[
- AmuletRules_CreateExternalPartySetupProposal.Arg,
- AmuletRules_CreateExternalPartySetupProposal.Res,
- ],
- ): State = {
- val validatorParty = Codec
- .decode(Codec.Party)(node.result.value.validator)
- .getOrElse(
- throw Status.INTERNAL
- .withDescription(
- s"Cannot decode party ID ${node.argument.value.validator}"
- )
- .asRuntimeException()
- )
- val transferResult = node.result.value.transferResult
- fromTransferPreapprovalPurchase(
- eventId,
- synchronizerId,
- validatorParty,
- transferResult,
- )
- }
-
- def fromCreateTransferPreapproval(
- eventId: String,
- synchronizerId: SynchronizerId,
- node: ExerciseNode[
- AmuletRules_CreateTransferPreapproval.Arg,
- AmuletRules_CreateTransferPreapproval.Res,
- ],
- ): State = {
- val validatorParty = Codec
- .decode(Codec.Party)(node.argument.value.provider)
- .getOrElse(
- throw Status.INTERNAL
- .withDescription(
- s"Cannot decode party ID ${node.argument.value.provider}"
- )
- .asRuntimeException()
- )
- val transferResult = node.result.value.transferResult
- fromTransferPreapprovalPurchase(
- eventId,
- synchronizerId,
- validatorParty,
- transferResult,
- )
- }
-
- def fromRenewTransferPreapproval(
- eventId: String,
- synchronizerId: SynchronizerId,
- node: ExerciseNode[
- TransferPreapproval_Renew.Arg,
- TransferPreapproval_Renew.Res,
- ],
- ): State = {
- val validatorParty = Codec
- .decode(Codec.Party)(node.result.value.provider)
- .getOrElse(
- throw Status.INTERNAL
- .withDescription(
- s"Cannot decode party ID ${node.result.value.provider}"
- )
- .asRuntimeException()
- )
- val transferResult = node.result.value.transferResult
- fromTransferPreapprovalPurchase(
- eventId,
- synchronizerId,
- validatorParty,
- transferResult,
- )
- }
-
- private def fromTransferPreapprovalPurchase(
- eventId: String,
- synchronizerId: SynchronizerId,
- validatorParty: PartyId,
- transferResult: TransferResult,
- ) = {
- val round = transferResult.round
-
- val rewardEntries = rewardsEntriesFromTransferSummary(
- validatorParty,
- transferResult.summary,
- round.number,
- synchronizerId,
- eventId,
- )
-
- State.empty.appended(rewardEntries)
- }
-
def fromOpenMiningRoundCreate(
eventId: String,
synchronizerId: SynchronizerId,
@@ -1181,25 +318,4 @@ object ScanTxLogParser {
)
}
}
-
- private def senderAmountNoFees(party: String, amount: BigDecimal) =
- SenderAmount(
- party = PartyId.tryFromProtoPrimitive(party),
- inputAmuletAmount = amount,
- inputAppRewardAmount = BigDecimal(0.0),
- inputValidatorRewardAmount = BigDecimal(0.0),
- senderChangeAmount = BigDecimal(0.0),
- senderChangeFee = BigDecimal(0.0),
- senderFee = BigDecimal(0.0),
- holdingFees = BigDecimal(0.0),
- inputSvRewardAmount = None,
- inputValidatorFaucetAmount = None,
- )
-
- private def receiverAmountNoFees(party: String, amount: BigDecimal) =
- ReceiverAmount(
- party = PartyId.tryFromProtoPrimitive(party),
- amount = amount,
- receiverFee = BigDecimal(0.0),
- )
}
diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/TxLogEntry.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/TxLogEntry.scala
index 2b17fdf90b..d51aa84ee9 100644
--- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/TxLogEntry.scala
+++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/TxLogEntry.scala
@@ -3,21 +3,11 @@
package org.lfdecentralizedtrust.splice.scan.store
-import org.lfdecentralizedtrust.splice.codegen.java.splice
import org.lfdecentralizedtrust.splice.store.StoreErrors
-import org.lfdecentralizedtrust.splice.util.Codec
-import scala.collection.immutable
-import scala.jdk.CollectionConverters.*
-import scala.jdk.OptionConverters.*
import java.time.Instant
import org.lfdecentralizedtrust.splice.http.v0.definitions as httpDef
-import org.lfdecentralizedtrust.splice.http.v0.definitions.TransactionHistoryResponseItem.TransactionType as HttpTransactionType
import com.digitalasset.canton.config.CantonRequireTypes.String3
-import com.digitalasset.canton.topology.PartyId
-
-import java.time.ZoneOffset
-import scala.math.BigDecimal.RoundingMode
trait TxLogEntry extends Product with Serializable {
// Scan store uses the eventId for pagination
@@ -28,42 +18,33 @@ object TxLogEntry extends StoreErrors {
object EntryType {
val ErrorTxLogEntry = String3.tryCreate("err")
- val BalanceChangeTxLogEntry = String3.tryCreate("bac")
val ClosedMiningRoundTxLogEntry = String3.tryCreate("cmr")
- val ExtraTrafficPurchaseTxLogEntry = String3.tryCreate("etp")
val OpenMiningRoundTxLogEntry = String3.tryCreate("omr")
- val AppRewardTxLogEntry = String3.tryCreate("are")
- val MintTxLogEntry = String3.tryCreate("min")
- val TapTxLogEntry = String3.tryCreate("tap")
- val TransferTxLogEntry = String3.tryCreate("tra")
- val ValidatorRewardTxLogEntry = String3.tryCreate("vre")
- val SvRewardTxLogEntry = String3.tryCreate("sre")
val VoteRequestTxLogEntry = String3.tryCreate("vot")
val TransferCommandTxLogEntry = String3.tryCreate("trc")
- val AbortTransferInstructionTxLogEntry = String3.tryCreate("ati")
// The following entry types correspond to entries that were removed from `scan_tx_log.proto`
// Those entries might still exist in databases, but we don't produce new ones and we don't read them.
// The values are only kept for documentation purposes.
val Unused_SvRewardCollectedTxLogEntry = String3.tryCreate("src")
+ val Unused_BalanceChangeTxLogEntry = String3.tryCreate("bac")
+ val Unused_ExtraTrafficPurchaseTxLogEntry = String3.tryCreate("etp")
+ val Unused_AppRewardTxLogEntry = String3.tryCreate("are")
+ val Unused_MintTxLogEntry = String3.tryCreate("min")
+ val Unused_TapTxLogEntry = String3.tryCreate("tap")
+ val Unused_TransferTxLogEntry = String3.tryCreate("tra")
+ val Unused_ValidatorRewardTxLogEntry = String3.tryCreate("vre")
+ val Unused_SvRewardTxLogEntry = String3.tryCreate("sre")
+ val Unused_AbortTransferInstructionTxLogEntry = String3.tryCreate("ati")
}
def encode(entry: TxLogEntry): (String3, String) = {
import scalapb.json4s.JsonFormat
val entryType = entry match {
case _: ErrorTxLogEntry => EntryType.ErrorTxLogEntry
- case _: BalanceChangeTxLogEntry => EntryType.BalanceChangeTxLogEntry
case _: ClosedMiningRoundTxLogEntry => EntryType.ClosedMiningRoundTxLogEntry
- case _: ExtraTrafficPurchaseTxLogEntry => EntryType.ExtraTrafficPurchaseTxLogEntry
case _: OpenMiningRoundTxLogEntry => EntryType.OpenMiningRoundTxLogEntry
- case _: AppRewardTxLogEntry => EntryType.AppRewardTxLogEntry
- case _: MintTxLogEntry => EntryType.MintTxLogEntry
- case _: TapTxLogEntry => EntryType.TapTxLogEntry
- case _: TransferTxLogEntry => EntryType.TransferTxLogEntry
- case _: ValidatorRewardTxLogEntry => EntryType.ValidatorRewardTxLogEntry
- case _: SvRewardTxLogEntry => EntryType.SvRewardTxLogEntry
case _: VoteRequestTxLogEntry => EntryType.VoteRequestTxLogEntry
case _: TransferCommandTxLogEntry => EntryType.TransferCommandTxLogEntry
- case _: AbortTransferInstructionTxLogEntry => EntryType.AbortTransferInstructionTxLogEntry
case _ => throw txEncodingFailed()
}
val jsonValue = entry match {
@@ -77,20 +58,10 @@ object TxLogEntry extends StoreErrors {
try {
entryType match {
case EntryType.ErrorTxLogEntry => from[ErrorTxLogEntry](json)
- case EntryType.BalanceChangeTxLogEntry => from[BalanceChangeTxLogEntry](json)
case EntryType.ClosedMiningRoundTxLogEntry => from[ClosedMiningRoundTxLogEntry](json)
- case EntryType.ExtraTrafficPurchaseTxLogEntry => from[ExtraTrafficPurchaseTxLogEntry](json)
case EntryType.OpenMiningRoundTxLogEntry => from[OpenMiningRoundTxLogEntry](json)
- case EntryType.AppRewardTxLogEntry => from[AppRewardTxLogEntry](json)
- case EntryType.MintTxLogEntry => from[MintTxLogEntry](json)
- case EntryType.TapTxLogEntry => from[TapTxLogEntry](json)
- case EntryType.TransferTxLogEntry => from[TransferTxLogEntry](json)
- case EntryType.ValidatorRewardTxLogEntry => from[ValidatorRewardTxLogEntry](json)
- case EntryType.SvRewardTxLogEntry => from[ValidatorRewardTxLogEntry](json)
case EntryType.VoteRequestTxLogEntry => from[VoteRequestTxLogEntry](json)
case EntryType.TransferCommandTxLogEntry => from[TransferCommandTxLogEntry](json)
- case EntryType.AbortTransferInstructionTxLogEntry =>
- from[AbortTransferInstructionTxLogEntry](json)
case _ => throw txLogIsOfWrongType(entryType.str)
}
} catch {
@@ -98,14 +69,6 @@ object TxLogEntry extends StoreErrors {
}
}
- trait RewardTxLogEntry extends TxLogEntry {
- def party: PartyId
-
- def amount: BigDecimal
-
- def round: Long
- }
-
trait TransactionTxLogEntry extends TxLogEntry {
def date: Option[Instant]
}
@@ -118,130 +81,6 @@ object TxLogEntry extends StoreErrors {
val Failed = "failed"
}
- private def toResponse(data: SenderAmount) = httpDef.SenderAmount(
- party = data.party.toProtoPrimitive,
- inputAmuletAmount = Some(Codec.encode(data.inputAmuletAmount)),
- inputAppRewardAmount = Some(Codec.encode(data.inputAppRewardAmount)),
- inputValidatorRewardAmount = Some(Codec.encode(data.inputValidatorRewardAmount)),
- inputSvRewardAmount = Some(Codec.encode(data.inputSvRewardAmount.getOrElse(BigDecimal(0)))),
- inputValidatorFaucetAmount = data.inputValidatorFaucetAmount.map(fa => Codec.encode(fa)),
- senderChangeAmount = Codec.encode(data.senderChangeAmount),
- senderChangeFee = Codec.encode(data.senderChangeFee),
- senderFee = Codec.encode(data.senderFee),
- holdingFees = Codec.encode(data.holdingFees),
- )
-
- private def toResponse(data: ReceiverAmount) = httpDef.ReceiverAmount(
- party = data.party.toProtoPrimitive,
- amount = Codec.encode(data.amount),
- receiverFee = Codec.encode(data.receiverFee),
- )
-
- private def toResponse(data: BalanceChange) = httpDef.BalanceChange(
- party = data.party.toProtoPrimitive,
- changeToInitialAmountAsOfRoundZero = Codec.encode(data.changeToInitialAmountAsOfRoundZero),
- changeToHoldingFeesRate = Codec.encode(data.changeToHoldingFeesRate),
- )
-
- private def toTransferResponseItem(entry: TransferTxLogEntry) =
- httpDef.TransactionHistoryResponseItem(
- transactionType = HttpTransactionType.Transfer,
- eventId = entry.eventId,
- offset = Some(entry.offset),
- domainId = entry.domainId.toProtoPrimitive,
- date = java.time.OffsetDateTime
- .ofInstant(entry.date.getOrElse(throw txMissingField()), ZoneOffset.UTC),
- transfer = Some(
- httpDef.Transfer(
- sender = toResponse(entry.sender.getOrElse(throw txMissingField())),
- receivers = entry.receivers.map(toResponse).toVector,
- balanceChanges = entry.balanceChanges.map(toResponse).toVector,
- description = Some(entry.description).filter(_.nonEmpty),
- transferInstructionReceiver =
- Some(entry.transferInstructionReceiver).filter(_.nonEmpty),
- transferInstructionAmount = entry.transferInstructionAmount.map(Codec.encode(_)),
- transferInstructionCid = Some(entry.transferInstructionCid).filter(_.nonEmpty),
- transferKind = entry.transferKind match {
- case TransferKind.Unrecognized(_) => None
- case TransferKind.TRANSFER_KIND_OTHER => None
- case TransferKind.TRANSFER_KIND_CREATE_TRANSFER_INSTRUCTION =>
- Some(httpDef.Transfer.TransferKind.members.CreateTransferInstruction)
- case TransferKind.TRANSFER_KIND_TRANSFER_INSTRUCTION_ACCEPT =>
- Some(httpDef.Transfer.TransferKind.members.TransferInstructionAccept)
- case TransferKind.TRANSFER_KIND_PREAPPROVAL_SEND =>
- Some(httpDef.Transfer.TransferKind.members.PreapprovalSend)
- },
- )
- ),
- round = Some(entry.round),
- )
-
- private def toTapResponseItem(entry: TapTxLogEntry) = httpDef.TransactionHistoryResponseItem(
- transactionType = HttpTransactionType.DevnetTap,
- eventId = entry.eventId,
- offset = Some(entry.offset),
- domainId = entry.domainId.toProtoPrimitive,
- date = java.time.OffsetDateTime
- .ofInstant(entry.date.getOrElse(throw txMissingField()), ZoneOffset.UTC),
- tap = Some(
- httpDef.AmuletAmount(
- amuletOwner = entry.amuletOwner.toProtoPrimitive,
- amuletAmount = Codec.encode(entry.amuletAmount),
- )
- ),
- round = Some(entry.round),
- )
-
- private def toMintResponseItem(entry: MintTxLogEntry) = httpDef.TransactionHistoryResponseItem(
- transactionType = HttpTransactionType.Mint,
- eventId = entry.eventId,
- offset = Some(entry.offset),
- domainId = entry.domainId.toProtoPrimitive,
- date = java.time.OffsetDateTime
- .ofInstant(entry.date.getOrElse(throw txMissingField()), ZoneOffset.UTC),
- mint = Some(
- httpDef.AmuletAmount(
- amuletOwner = entry.amuletOwner.toProtoPrimitive,
- amuletAmount = Codec.encode(entry.amuletAmount),
- )
- ),
- )
-
- private def toAbortTransferInstructionResponseItem(entry: AbortTransferInstructionTxLogEntry) =
- httpDef.TransactionHistoryResponseItem(
- transactionType = HttpTransactionType.AbortTransferInstruction,
- eventId = entry.eventId,
- offset = Some(entry.offset),
- domainId = entry.domainId.toProtoPrimitive,
- date = java.time.OffsetDateTime
- .ofInstant(entry.date.getOrElse(throw txMissingField()), ZoneOffset.UTC),
- abortTransferInstruction = Some(
- httpDef.AbortTransferInstruction(
- abortKind = entry.transferAbortKind match {
- case TransferAbortKind.Unrecognized(_) =>
- sys.error(s"Unexpected transfer abort kind: ${entry.transferAbortKind}")
- case TransferAbortKind.TRANSFER_ABORT_KIND_RESERVED =>
- sys.error(s"Unexpected transfer abort kind: ${entry.transferAbortKind}")
- case TransferAbortKind.TRANSFER_ABORT_KIND_REJECT =>
- httpDef.AbortTransferInstruction.AbortKind.members.Reject
- case TransferAbortKind.TRANSFER_ABORT_KIND_WITHDRAW =>
- httpDef.AbortTransferInstruction.AbortKind.members.Withdraw
- },
- transferInstructionCid = entry.transferInstructionCid,
- )
- ),
- )
-
- def toResponseItem(entry: TransactionTxLogEntry): httpDef.TransactionHistoryResponseItem =
- entry match {
- case entry: TransferTxLogEntry => toTransferResponseItem(entry)
- case entry: TapTxLogEntry => toTapResponseItem(entry)
- case entry: MintTxLogEntry => toMintResponseItem(entry)
- case entry: AbortTransferInstructionTxLogEntry =>
- toAbortTransferInstructionResponseItem(entry)
- case _ => throw txLogIsOfWrongType(entry.getClass.getSimpleName)
- }
-
def toResponse(
status: TransferCommandTxLogEntry.Status
): httpDef.TransferCommandContractStatus =
@@ -275,101 +114,4 @@ object TxLogEntry extends StoreErrors {
)
}
}
-
- sealed trait TransactionType
- object TransactionType {
- case object Transfer extends TransactionType
- case object Mint extends TransactionType
- case object Tap extends TransactionType
- }
-
- def parseSenderAmount(
- sender: String,
- outputs: Seq[splice.amuletrules.TransferOutput],
- res: splice.amuletrules.TransferResult,
- ): SenderAmount = {
- val senderFee = parseOutputAmounts(outputs, res)
- .map(_.senderFee)
- .sum
-
- SenderAmount(
- party = PartyId.tryFromProtoPrimitive(sender),
- inputAmuletAmount = res.summary.inputAmuletAmount,
- inputAppRewardAmount = res.summary.inputAppRewardAmount,
- inputValidatorRewardAmount = res.summary.inputValidatorRewardAmount,
- inputSvRewardAmount = Some(res.summary.inputSvRewardAmount),
- inputValidatorFaucetAmount =
- res.summary.inputValidatorFaucetAmount.toScala.map(BigDecimal(_)),
- senderChangeAmount = res.summary.senderChangeAmount,
- senderChangeFee = res.summary.senderChangeFee,
- senderFee = senderFee,
- holdingFees = res.summary.holdingFees,
- )
- }
-
- def parseReceiverAmounts(
- outputs: Seq[splice.amuletrules.TransferOutput],
- res: splice.amuletrules.TransferResult,
- ): Seq[ReceiverAmount] = {
-
- // Note: the same receiver party can appear multiple times in the transfer result
- // The code below merges amounts and fees for the same receiver, while preserving
- // the order of receivers.
- parseOutputAmounts(outputs, res)
- .map(o =>
- new ReceiverAmount(
- party = PartyId.tryFromProtoPrimitive(o.output.receiver),
- amount = o.output.amount,
- receiverFee = o.receiverFee,
- )
- )
- .foldLeft(immutable.ListMap.empty[PartyId, ReceiverAmount])((acc, receiverAmount) =>
- acc.updatedWith(receiverAmount.party)(prev =>
- Some(prev.fold(receiverAmount) { r =>
- r.copy(
- amount = r.amount + receiverAmount.amount,
- receiverFee = r.receiverFee + receiverAmount.receiverFee,
- )
- })
- )
- )
- .values
- .toList
- }
-
- /** A requested output of a transfer, together with the actual fees paid for the transfer.
- *
- * @param output Contains the receiver and the gross amount received (before deducting fees).
- * @param senderFee Actual amount of fees paid by the sender.
- * @param receiverFee Actual amount of fees paid by the receiver.
- */
- private final case class OutputWithFees(
- output: splice.amuletrules.TransferOutput,
- senderFee: BigDecimal,
- receiverFee: BigDecimal,
- )
-
- private def parseOutputAmounts(
- outputs: Seq[splice.amuletrules.TransferOutput],
- res: splice.amuletrules.TransferResult,
- ): Seq[OutputWithFees] = {
- assert(
- outputs.size == res.summary.outputFees.size(),
- "Each output should have a corresponding fee",
- )
- val outputsWithFees = outputs.zip(res.summary.outputFees.asScala)
-
- outputsWithFees
- .map { case (out, fee) =>
- OutputWithFees(
- output = out,
- senderFee = setDamlDecimalScale(BigDecimal(fee) * (BigDecimal(1) - out.receiverFeeRatio)),
- receiverFee = setDamlDecimalScale(BigDecimal(fee) * out.receiverFeeRatio),
- )
- }
- }
-
- /** Returns the input number modified such that it has the same number of decimal places as a daml decimal */
- private def setDamlDecimalScale(x: BigDecimal): BigDecimal =
- x.setScale(10, RoundingMode.HALF_EVEN)
}
diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanStore.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanStore.scala
index 92fbc43bf0..871c2633b6 100644
--- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanStore.scala
+++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/DbScanStore.scala
@@ -60,9 +60,7 @@ import org.lfdecentralizedtrust.splice.store.{
DbVotesTxLogStoreQueryBuilder,
Limit,
VoteResultsFilters,
- PageLimit,
ResultsPage,
- SortOrder,
TxLogStore,
UpdateHistory,
}
@@ -79,7 +77,6 @@ import org.lfdecentralizedtrust.splice.config.IngestionConfig
import org.lfdecentralizedtrust.splice.store.UpdateHistoryQueries.UpdateHistoryQueries
import org.lfdecentralizedtrust.splice.store.db.AcsQueries.AcsStoreId
import org.lfdecentralizedtrust.splice.store.db.TxLogQueries.TxLogStoreId
-import slick.jdbc.canton.SQLActionBuilder
import scala.concurrent.{ExecutionContext, Future}
import scala.jdk.CollectionConverters.*
@@ -391,63 +388,6 @@ class DbScanStore(
} yield contractWithStateFromRow(TransferCommandCounter.COMPANION)(row)).value
}
- override def listTransactions(
- pageEndEventId: Option[String],
- sortOrder: SortOrder,
- limit: PageLimit,
- )(implicit
- tc: TraceContext
- ): Future[Seq[TxLogEntry.TransactionTxLogEntry]] =
- waitUntilAcsIngested {
- val entryTypeCondition: SQLActionBuilder = inClause(
- "entry_type",
- List(
- EntryType.TransferTxLogEntry,
- EntryType.TapTxLogEntry,
- EntryType.MintTxLogEntry,
- EntryType.AbortTransferInstructionTxLogEntry,
- ),
- )
- // Literal sort order since Postgres complains when trying to bind it to a parameter
- val (compareEntryNumber, orderLimit) = sortOrder match {
- case SortOrder.Ascending =>
- (sql" > ", sql""" order by entry_number asc limit ${sqlLimit(limit)};""")
- case SortOrder.Descending =>
- (sql" < ", sql""" order by entry_number desc limit ${sqlLimit(limit)};""")
- }
-
- // TODO (#960): don't use the event id for pagination, use the entry number
- for {
- rows <- storage.query(
- pageEndEventId.fold(
- selectFromTxLogTable(
- txLogTableName,
- txLogStoreId,
- where = entryTypeCondition,
- orderLimit = orderLimit,
- )
- )(pageEndEventId =>
- selectFromTxLogTable(
- txLogTableName,
- txLogStoreId,
- where = (entryTypeCondition ++ sql" and entry_number " ++ compareEntryNumber ++
- sql"""(
- select entry_number
- from scan_txlog_store
- where store_id = $txLogStoreId
- and event_id = ${lengthLimited(pageEndEventId)}
- and """ ++ entryTypeCondition ++ sql"""
- )""").toActionBuilder,
- orderLimit = orderLimit,
- )
- ),
- "listTransactions",
- )
- entries = rows.map(txLogEntryFromRow[TxLogEntry.TransactionTxLogEntry](txLogConfig))
- } yield entries
-
- }
-
override def lookupFeaturedAppRight(
providerPartyId: PartyId
)(implicit
diff --git a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/ScanTables.scala b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/ScanTables.scala
index bba1683131..de109b94bf 100644
--- a/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/ScanTables.scala
+++ b/apps/scan/src/main/scala/org/lfdecentralizedtrust/splice/scan/store/db/ScanTables.scala
@@ -15,19 +15,10 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.dsorules.actionrequir
}
import org.lfdecentralizedtrust.splice.codegen.java.splice.externalpartyamuletrules.TransferCommand
import org.lfdecentralizedtrust.splice.scan.store.{
- AbortTransferInstructionTxLogEntry,
- AppRewardTxLogEntry,
- BalanceChangeTxLogEntry,
ClosedMiningRoundTxLogEntry,
ErrorTxLogEntry,
- ExtraTrafficPurchaseTxLogEntry,
- MintTxLogEntry,
OpenMiningRoundTxLogEntry,
- SvRewardTxLogEntry,
- TapTxLogEntry,
- TransferTxLogEntry,
TxLogEntry,
- ValidatorRewardTxLogEntry,
VoteRequestTxLogEntry,
TransferCommandTxLogEntry,
}
@@ -193,50 +184,6 @@ object ScanTables extends AcsTables {
round = Some(cmr.round),
closedRoundEffectiveAt = cmr.effectiveAt.map(CantonTimestamp.assertFromInstant),
)
- case are: AppRewardTxLogEntry =>
- ScanTxLogRowData(
- entry = are,
- round = Some(are.round),
- rewardAmount = Some(are.amount),
- rewardedParty = Some(are.party),
- )
- case vre: ValidatorRewardTxLogEntry =>
- ScanTxLogRowData(
- entry = vre,
- round = Some(vre.round),
- rewardAmount = Some(vre.amount),
- rewardedParty = Some(vre.party),
- )
- case sre: SvRewardTxLogEntry =>
- ScanTxLogRowData(
- entry = sre,
- round = Some(sre.round),
- rewardAmount = Some(sre.amount),
- rewardedParty = Some(sre.party),
- )
- case etp: ExtraTrafficPurchaseTxLogEntry =>
- ScanTxLogRowData(
- entry = etp,
- round = Some(etp.round),
- extraTrafficValidator = Some(etp.validator),
- extraTrafficPurchaseTrafficPurchase = Some(etp.trafficPurchased),
- extraTrafficPurchaseCcSpent = Some(etp.ccSpent),
- )
- case rar: TransferTxLogEntry =>
- ScanTxLogRowData(
- entry = rar,
- round = Some(rar.round),
- )
- case entry: TapTxLogEntry =>
- ScanTxLogRowData(
- entry = entry,
- round = Some(entry.round),
- )
- case entry: MintTxLogEntry =>
- ScanTxLogRowData(
- entry = entry,
- round = Some(entry.round),
- )
case vr: VoteRequestTxLogEntry =>
val result = vr.result.getOrElse(throw txMissingField())
val parsedOutcome = VoteRequestOutcome.parse(result.outcome)
@@ -268,20 +215,12 @@ object ScanTables extends AcsTables {
entry.nonce
),
)
- case entry: AbortTransferInstructionTxLogEntry =>
- ScanTxLogRowData(
- entry = entry
- )
case _ =>
throw txEncodingFailed()
}
}
record match {
- case _: BalanceChangeTxLogEntry =>
- // the balance changes are no longer indexed, or written, to the tx log table,
- // See https://github.com/canton-network/splice/pull/3734
- None
case entry => Some(fromEntry(entry))
}
}
diff --git a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/ScanStoreTest.scala b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/ScanStoreTest.scala
index b84bda45f0..3d2271e786 100644
--- a/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/ScanStoreTest.scala
+++ b/apps/scan/src/test/scala/org/lfdecentralizedtrust/splice/store/db/ScanStoreTest.scala
@@ -57,7 +57,6 @@ import scala.concurrent.Future
import scala.jdk.CollectionConverters.*
import scala.jdk.OptionConverters.*
import scala.math.BigDecimal.javaBigDecimal2bigDecimal
-import scala.reflect.ClassTag
import org.lfdecentralizedtrust.splice.config.IngestionConfig
import org.lfdecentralizedtrust.splice.store.MultiDomainAcsStore.IngestionSink.IngestionStart.{
InitializeAcsAtLatestOffset,
@@ -332,144 +331,6 @@ abstract class ScanStoreTest
}
}
- "listTransactions" should {
- "return the most recent txs in pages" in {
- val limit = 10
- val nrTransfers = 20
- val round = 1L
- val now = java.time.Instant.EPOCH
- val zero = BigDecimal(0)
- val fakeOffset = "0"
- val txs: List[TransferTxLogEntry] = (1 to nrTransfers).map { i =>
- TransferTxLogEntry(
- offset = fakeOffset,
- eventId = s"$i",
- domainId = dummyDomain,
- date = Some(now),
- sender = Some(
- SenderAmount(
- user1,
- BigDecimal(i),
- zero,
- zero,
- zero,
- zero,
- zero,
- zero,
- Some(zero),
- None,
- )
- ),
- balanceChanges = Seq(),
- receivers = Seq(ReceiverAmount(user2, BigDecimal(i), zero)),
- round = round,
- )
- }.toList
- def stripEventIdAndOffset(tx: TransferTxLogEntry) =
- tx.copy(eventId = "", offset = fakeOffset)
- val expectedFirstPage = txs.reverse.take(limit).toList
- val expectedSecondPage = txs.reverse.drop(limit).take(limit).toList
-
- def transferFromTransaction(
- store: ScanStore,
- amuletRulesContract: Contract[
- splice.amuletrules.AmuletRules.ContractId,
- splice.amuletrules.AmuletRules,
- ],
- tx: TransferTxLogEntry,
- ) = {
- val sender = tx.sender.getOrElse(throw txMissingField())
- val senderParty = sender.party
- val senderAmount = sender.inputAmuletAmount
- val receiverParty = tx.receivers(0).party
- val receiverAmount = tx.receivers(0).amount
- dummyDomain
- .exercise(
- contract = amuletRulesContract,
- interfaceId = Some(splice.amuletrules.AmuletRules.TEMPLATE_ID_WITH_PACKAGE_ID),
- choiceName = Transfer.choice.name,
- choiceArgument = mkAmuletRules_Transfer(
- mkTransferInputOutput(
- senderParty,
- senderParty,
- List(mkInputAmulet()),
- List(mkTransferOutput(receiverParty, receiverAmount)),
- )
- ),
- exerciseResult = mkTransferResultRecord(
- round = round,
- inputAppRewardAmount = sender.inputAppRewardAmount.toDouble,
- inputAmuletAmount = senderAmount.toDouble,
- inputValidatorRewardAmount = sender.inputValidatorRewardAmount.toDouble,
- inputSvRewardAmount = sender.inputSvRewardAmount.fold(0.0)(_.toDouble),
- balanceChanges = Map(),
- amuletPrice = 1.0,
- ),
- )(
- store.multiDomainAcsStore
- )
- .map(_ => ())
- }
-
- for {
- store <- mkStore()
- amuletRulesContract = amuletRules()
- _ <- txs.foldLeft(Future.successful(())) { (f, tx) =>
- f.flatMap { _ =>
- transferFromTransaction(
- store,
- amuletRulesContract,
- tx,
- )
- }
- }
- } yield {
- val firstPageDescending = store
- .listByType[TransferTxLogEntry](None, SortOrder.Descending, limit)
- .futureValue
- .toList
-
- firstPageDescending
- .map(stripEventIdAndOffset) should be(
- expectedFirstPage
- .map(stripEventIdAndOffset)
- )
- val nextPageDescending = store
- .listByType[TransferTxLogEntry](
- Some(firstPageDescending.last.eventId),
- SortOrder.Descending,
- limit,
- )
- .futureValue
- .toList
-
- nextPageDescending
- .map(stripEventIdAndOffset) should be(
- expectedSecondPage
- .map(stripEventIdAndOffset)
- )
-
- val firstPageAscending = store
- .listByType[TransferTxLogEntry](None, SortOrder.Ascending, limit)
- .futureValue
- .toList
-
- firstPageAscending should be(nextPageDescending.reverse)
-
- val nextPageAscending = store
- .listByType[TransferTxLogEntry](
- Some(firstPageAscending.last.eventId),
- SortOrder.Ascending,
- limit,
- )
- .futureValue
- .toList
-
- nextPageAscending should be(firstPageDescending.reverse)
- }
- }
- }
-
"votes" should {
"listVoteRequestResults" should {
@@ -1401,20 +1262,6 @@ abstract class ScanStoreTest
): Future[UpdateHistory]
private lazy val user1 = userParty(1)
- private lazy val user2 = userParty(2)
-
- implicit class ScanStoreExt(store: ScanStore) {
- @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
- def listByType[T](beginAfterEventId: Option[String], sortOrder: SortOrder, limit: Int)(implicit
- tag: ClassTag[T]
- ): Future[Seq[T]] = {
- store
- .listTransactions(beginAfterEventId, sortOrder, PageLimit.tryCreate(limit))
- .map(_.collect {
- case c if tag.runtimeClass.isInstance(c) => c.asInstanceOf[T]
- }.toSeq)
- }
- }
}
trait AmuletTransferUtil { self: StoreTestBase =>
def mkInputAmulet() = {
diff --git a/apps/sv/src/main/openapi/sv-internal.yaml b/apps/sv/src/main/openapi/sv-internal.yaml
index 3ac9285a06..f20abd07ef 100644
--- a/apps/sv/src/main/openapi/sv-internal.yaml
+++ b/apps/sv/src/main/openapi/sv-internal.yaml
@@ -176,7 +176,7 @@ paths:
/v0/admin/synchronizer/lsu/cancel:
post:
tags: [sv]
- x-jvm-package: sv_admin
+ x-jvm-package: sv_operator
operationId: "cancelLogicalSynchronizerUpgrade"
responses:
"200":
diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/SvApp.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/SvApp.scala
index b7fcb81919..63e562ce9e 100644
--- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/SvApp.scala
+++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/SvApp.scala
@@ -498,6 +498,7 @@ class SvApp(
timeouts,
loggerFactory,
amuletAppParameters.upgradesConfig,
+ participantAdminConnection,
)
adminHandler = new HttpSvAdminHandler(
diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvAdminAppClient.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvAdminAppClient.scala
index 6e9060523e..0f6cafa261 100644
--- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvAdminAppClient.scala
+++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvAdminAppClient.scala
@@ -40,22 +40,4 @@ object HttpSvAdminAppClient {
}
}
- case class CancelLogicalSynchronizerUpgrade()
- extends BaseCommand[http.CancelLogicalSynchronizerUpgradeResponse, Unit] {
-
- override def submitRequest(
- client: Client,
- headers: List[HttpHeader],
- ): EitherT[Future, Either[
- Throwable,
- HttpResponse,
- ], http.CancelLogicalSynchronizerUpgradeResponse] =
- client.cancelLogicalSynchronizerUpgrade(headers = headers)
-
- override def handleOk()(implicit
- decoder: TemplateJsonDecoder
- ) = { case http.CancelLogicalSynchronizerUpgradeResponse.OK =>
- Right(())
- }
- }
}
diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvOperatorAppClient.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvOperatorAppClient.scala
index 0a998cfacc..2744a82274 100644
--- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvOperatorAppClient.scala
+++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvOperatorAppClient.scala
@@ -468,4 +468,23 @@ object HttpSvOperatorAppClient {
Left(response.error)
}
}
+
+ case class CancelLogicalSynchronizerUpgrade()
+ extends BaseCommand[http.CancelLogicalSynchronizerUpgradeResponse, Unit] {
+
+ override def submitRequest(
+ client: Client,
+ headers: List[HttpHeader],
+ ): EitherT[Future, Either[
+ Throwable,
+ HttpResponse,
+ ], http.CancelLogicalSynchronizerUpgradeResponse] =
+ client.cancelLogicalSynchronizerUpgrade(headers = headers)
+
+ override def handleOk()(implicit
+ decoder: TemplateJsonDecoder
+ ) = { case http.CancelLogicalSynchronizerUpgradeResponse.OK =>
+ Right(())
+ }
+ }
}
diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvAdminHandler.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvAdminHandler.scala
index a39f4e38b0..8ff0a70ae8 100644
--- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvAdminHandler.scala
+++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvAdminHandler.scala
@@ -37,21 +37,6 @@ class HttpSvAdminHandler(
protected val workflowId: String = this.getClass.getSimpleName
private val dsoStore = dsoStoreWithIngestion.store
- override def cancelLogicalSynchronizerUpgrade(
- respond: r0.CancelLogicalSynchronizerUpgradeResponse.type
- )()(
- extracted: AdminUserRequest
- ): Future[r0.CancelLogicalSynchronizerUpgradeResponse] = {
- implicit val AdminUserRequest(traceContext) = extracted
- withSpan(s"$workflowId.cancelLogicalSynchronizerUpgrade") { _ => _ =>
- for {
- decentralizedSynchronizer <- dsoStore.getDsoRules().map(_.domain)
- _ <- participantAdminConnection
- .removeLsuAnnouncement(decentralizedSynchronizer)
- } yield r0.CancelLogicalSynchronizerUpgradeResponseOK
- }
- }
-
override def getSynchronizerNodeIdentitiesDump(
respond: r0.GetSynchronizerNodeIdentitiesDumpResponse.type
)()(
diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvOperatorHandler.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvOperatorHandler.scala
index f9937ca058..5cf43e04a7 100644
--- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvOperatorHandler.scala
+++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvOperatorHandler.scala
@@ -62,6 +62,7 @@ class HttpSvOperatorHandler(
override protected val timeouts: ProcessingTimeout,
protected val loggerFactory: NamedLoggerFactory,
upgradesConfig: UpgradesConfig,
+ participantAdminConnection: ParticipantAdminConnection,
)(implicit
ec: ExecutionContextExecutor,
protected val tracer: Tracer,
@@ -597,6 +598,21 @@ class HttpSvOperatorHandler(
}
}
+ override def cancelLogicalSynchronizerUpgrade(
+ respond: r0.CancelLogicalSynchronizerUpgradeResponse.type
+ )()(
+ extracted: ActAsKnownUserRequest
+ ): Future[r0.CancelLogicalSynchronizerUpgradeResponse] = {
+ implicit val ActAsKnownUserRequest(traceContext) = extracted
+ withSpan(s"$workflowId.cancelLogicalSynchronizerUpgrade") { _ => _ =>
+ for {
+ decentralizedSynchronizer <- dsoStore.getDsoRules().map(_.domain)
+ _ <- participantAdminConnection
+ .removeLsuAnnouncement(decentralizedSynchronizer)
+ } yield r0.CancelLogicalSynchronizerUpgradeResponseOK
+ }
+ }
+
private def withClientOrNotFound[T](
notFound: definitions.ErrorResponse => T
)(call: CometBftClient => Future[T])(implicit tc: TraceContext) =
diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/ValidatorApp.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/ValidatorApp.scala
index d9b8dfb543..f9b9173550 100644
--- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/ValidatorApp.scala
+++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/ValidatorApp.scala
@@ -782,6 +782,7 @@ class ValidatorApp(
config.parameters.enabledFeatures,
config.additionalPackagesToUnvet,
config.domains.global.alias,
+ config.enableDeprecatedTransferCommandSupport,
loggerFactory,
packageVersionSupport,
)
diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorAdminHandler.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorAdminHandler.scala
index 650f7dbd32..4dc6ad2b79 100644
--- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorAdminHandler.scala
+++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorAdminHandler.scala
@@ -96,6 +96,16 @@ class HttpValidatorAdminHandler(
)(handleRequest)
}
+ private def requireTransferCommandSupport[T](handleRequest: => T): T = {
+ if (config.enableDeprecatedTransferCommandSupport) {
+ handleRequest
+ } else {
+ throw HttpErrorHandler.notImplemented(
+ "Transfer command support is disabled by default and will be removed in 0.8.0. You can temporarily enable in on 0.7.x by setting enable-deprecated-transfer-command-support=true in your validator config."
+ )
+ }
+ }
+
def onboardUser(
respond: v0.ValidatorAdminResource.OnboardUserResponse.type
)(
@@ -704,89 +714,91 @@ class HttpValidatorAdminHandler(
): Future[v0.ValidatorAdminResource.PrepareTransferPreapprovalSendResponse] = {
implicit val AdminUserRequest(tracedContext) = tuser
requireWalletEnabled { _ =>
- val senderParty = PartyId.tryFromProtoPrimitive(body.senderPartyId)
- val receiverParty = PartyId.tryFromProtoPrimitive(body.receiverPartyId)
- for {
- synchronizerId <- getAmuletRulesDomain()(tracedContext)
- // This check is just to make it fail early. The actual preapproval is fixed when the automation
- // executes the transfer but we want the user to get feedback during the prepare step already.
- _ <- scanConnection.lookupTransferPreapprovalByParty(receiverParty).map { preapprovalO =>
- if (preapprovalO.isEmpty) {
- throw Status.INVALID_ARGUMENT
- .withDescription(s"Receiver $receiverParty does not have a TransferPreapproval")
- .asRuntimeException
+ requireTransferCommandSupport {
+ val senderParty = PartyId.tryFromProtoPrimitive(body.senderPartyId)
+ val receiverParty = PartyId.tryFromProtoPrimitive(body.receiverPartyId)
+ for {
+ synchronizerId <- getAmuletRulesDomain()(tracedContext)
+ // This check is just to make it fail early. The actual preapproval is fixed when the automation
+ // executes the transfer but we want the user to get feedback during the prepare step already.
+ _ <- scanConnection.lookupTransferPreapprovalByParty(receiverParty).map { preapprovalO =>
+ if (preapprovalO.isEmpty) {
+ throw Status.INVALID_ARGUMENT
+ .withDescription(s"Receiver $receiverParty does not have a TransferPreapproval")
+ .asRuntimeException
+ }
}
- }
- externalPartyAmuletRules <- scanConnection.getExternalPartyAmuletRules()
- supportsDescription <- packageVersionSupport
- .supportsDescriptionInTransferPreapprovals(
- Seq(receiverParty, senderParty, store.key.dsoParty),
- clock.now,
- )
- .map(_.supported)
- commands = externalPartyAmuletRules.toAssignedContract
- .getOrElse(
- throw Status.Code.FAILED_PRECONDITION.toStatus
- .withDescription(
- s"ExternalPartyAmuletRules is currently inflight between synchronizers, retry until it is assigned to a synchronizer"
+ externalPartyAmuletRules <- scanConnection.getExternalPartyAmuletRules()
+ supportsDescription <- packageVersionSupport
+ .supportsDescriptionInTransferPreapprovals(
+ Seq(receiverParty, senderParty, store.key.dsoParty),
+ clock.now,
+ )
+ .map(_.supported)
+ commands = externalPartyAmuletRules.toAssignedContract
+ .getOrElse(
+ throw Status.Code.FAILED_PRECONDITION.toStatus
+ .withDescription(
+ s"ExternalPartyAmuletRules is currently inflight between synchronizers, retry until it is assigned to a synchronizer"
+ )
+ .asRuntimeException()
+ )
+ .exercise(
+ _.exerciseExternalPartyAmuletRules_CreateTransferCommand(
+ senderParty.toProtoPrimitive,
+ receiverParty.toProtoPrimitive,
+ store.key.validatorParty.toProtoPrimitive,
+ body.amount.bigDecimal,
+ body.expiresAt.toInstant,
+ body.nonce,
+ Option.when(supportsDescription)(body.description).flatten.toJava,
+ java.util.Optional.of(store.key.dsoParty.toProtoPrimitive),
)
- .asRuntimeException()
- )
- .exercise(
- _.exerciseExternalPartyAmuletRules_CreateTransferCommand(
- senderParty.toProtoPrimitive,
- receiverParty.toProtoPrimitive,
- store.key.validatorParty.toProtoPrimitive,
- body.amount.bigDecimal,
- body.expiresAt.toInstant,
- body.nonce,
- Option.when(supportsDescription)(body.description).flatten.toJava,
- java.util.Optional.of(store.key.dsoParty.toProtoPrimitive),
+ )
+ .update
+ .commands()
+ .asScala
+ .toSeq
+ r <- storeWithIngestion
+ .connection(SpliceLedgerConnectionPriority.Medium)
+ .prepareSubmission(
+ Some(synchronizerId),
+ Seq(senderParty),
+ Seq(senderParty),
+ commands,
+ storeWithIngestion
+ .connection(SpliceLedgerConnectionPriority.Medium)
+ .disclosedContracts(externalPartyAmuletRules),
+ body.verboseHashing.getOrElse(false),
+ )
+ transferCommandCid = r.preparedTransaction
+ .flatMap(_.transaction)
+ .toList
+ .flatMap(_.nodes)
+ .flatMap(n =>
+ n.getV1.nodeType match {
+ case interactive.transaction.v1.interactive_submission_data.Node.NodeType
+ .Create(create) =>
+ Seq(create.contractId)
+ case _ => Seq.empty
+ }
+ )
+ .headOption
+ .getOrElse(
+ throw Status.INTERNAL
+ .withDescription("Failed to obtain transferCommandCid from prepared transaction")
+ .asRuntimeException()
+ )
+ } yield {
+ v0.ValidatorAdminResource.PrepareTransferPreapprovalSendResponse.OK(
+ definitions.PrepareTransferPreapprovalSendResponse(
+ Base64.getEncoder.encodeToString(r.getPreparedTransaction.toByteArray),
+ HexString.toHexString(r.preparedTransactionHash),
+ transferCommandCid,
+ r.hashingDetails,
)
)
- .update
- .commands()
- .asScala
- .toSeq
- r <- storeWithIngestion
- .connection(SpliceLedgerConnectionPriority.Medium)
- .prepareSubmission(
- Some(synchronizerId),
- Seq(senderParty),
- Seq(senderParty),
- commands,
- storeWithIngestion
- .connection(SpliceLedgerConnectionPriority.Medium)
- .disclosedContracts(externalPartyAmuletRules),
- body.verboseHashing.getOrElse(false),
- )
- transferCommandCid = r.preparedTransaction
- .flatMap(_.transaction)
- .toList
- .flatMap(_.nodes)
- .flatMap(n =>
- n.getV1.nodeType match {
- case interactive.transaction.v1.interactive_submission_data.Node.NodeType
- .Create(create) =>
- Seq(create.contractId)
- case _ => Seq.empty
- }
- )
- .headOption
- .getOrElse(
- throw Status.INTERNAL
- .withDescription("Failed to obtain transferCommandCid from prepared transaction")
- .asRuntimeException()
- )
- } yield {
- v0.ValidatorAdminResource.PrepareTransferPreapprovalSendResponse.OK(
- definitions.PrepareTransferPreapprovalSendResponse(
- Base64.getEncoder.encodeToString(r.getPreparedTransaction.toByteArray),
- HexString.toHexString(r.preparedTransactionHash),
- transferCommandCid,
- r.hashingDetails,
- )
- )
+ }
}
}
}
@@ -798,15 +810,17 @@ class HttpValidatorAdminHandler(
): Future[v0.ValidatorAdminResource.SubmitTransferPreapprovalSendResponse] = {
implicit val AdminUserRequest(tracedContext) = tuser
requireWalletEnabled { _ =>
- for {
- updateId <- ValidatorUtil.submitAsExternalParty(
- storeWithIngestion.connection(SpliceLedgerConnectionPriority.Medium),
- body.submission,
- waitForOffset = false,
+ requireTransferCommandSupport {
+ for {
+ updateId <- ValidatorUtil.submitAsExternalParty(
+ storeWithIngestion.connection(SpliceLedgerConnectionPriority.Medium),
+ body.submission,
+ waitForOffset = false,
+ )
+ } yield v0.ValidatorAdminResource.SubmitTransferPreapprovalSendResponseOK(
+ definitions.SubmitTransferPreapprovalSendResponse(updateId)
)
- } yield v0.ValidatorAdminResource.SubmitTransferPreapprovalSendResponseOK(
- definitions.SubmitTransferPreapprovalSendResponse(updateId)
- )
+ }
}
}
diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorAutomationService.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorAutomationService.scala
index a5ea0ab1bb..fb4b1e42d0 100644
--- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorAutomationService.scala
+++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/automation/ValidatorAutomationService.scala
@@ -76,6 +76,7 @@ class ValidatorAutomationService(
enabledFeatures: EnabledFeaturesConfig,
additionalPackagesToUnvet: Map[PackageName, Set[PackageVersion]],
globalSynchronizerAlias: SynchronizerAlias,
+ enableDeprecatedTransferCommandSupport: Boolean,
override protected val loggerFactory: NamedLoggerFactory,
packageVersionSupport: PackageVersionSupport,
)(implicit
@@ -196,15 +197,17 @@ class ValidatorAutomationService(
)
)
- registerTrigger(
- new TransferCommandSendTrigger(
- triggerContext,
- scanConnection,
- store,
- walletManager.externalPartyWalletManager,
- connection(SpliceLedgerConnectionPriority.Medium),
+ if (enableDeprecatedTransferCommandSupport) {
+ registerTrigger(
+ new TransferCommandSendTrigger(
+ triggerContext,
+ scanConnection,
+ store,
+ walletManager.externalPartyWalletManager,
+ connection(SpliceLedgerConnectionPriority.Medium),
+ )
)
- )
+ }
}
backupDumpConfig.foreach(config =>
diff --git a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/config/ValidatorAppConfig.scala b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/config/ValidatorAppConfig.scala
index 9dde4c84da..2973fca980 100644
--- a/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/config/ValidatorAppConfig.scala
+++ b/apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/config/ValidatorAppConfig.scala
@@ -224,6 +224,8 @@ case class ValidatorAppBackendConfig(
// from running concurrently against the same database. Only disable for migration scenarios
// where intentional overlap is required.
instanceLockEnabled: Boolean = true,
+ // Enable the deprecated transfer command support, will be fully removed in 0.8.0.
+ enableDeprecatedTransferCommandSupport: Boolean = false,
) extends SpliceBackendConfig // TODO(DACH-NY/canton-network-node#736): fork or generalize this trait.
{
override val nodeTypeName: String = "validator"
diff --git a/build.sbt b/build.sbt
index cfb3f09da6..c6724d4e89 100644
--- a/build.sbt
+++ b/build.sbt
@@ -166,7 +166,7 @@ lazy val root: Project = (project in file("."))
BuildCommon.sharedSettings,
scalacOptions ++= Seq("-Wconf:src=src_managed/.*:silent"),
// Needed to be able to resolve scalafmt snapshot versions
- resolvers ++= Resolver.sonatypeOssRepos("snapshots"),
+ resolvers += Resolver.sonatypeCentralSnapshots,
damlDarsLockCheckerFileArg := {
val darFiles: Seq[File] = damlBuild.all(allDarsFilter).value.flatten
val basePath = baseDirectory.value.toPath
diff --git a/cluster/configs/shared/rate-limits/unlimited.yaml b/cluster/configs/shared/rate-limits/unlimited.yaml
index 65b069f8b8..9ea479e8fa 100644
--- a/cluster/configs/shared/rate-limits/unlimited.yaml
+++ b/cluster/configs/shared/rate-limits/unlimited.yaml
@@ -85,10 +85,6 @@ rateLimits:
name: dso-party-id
type: unlimited
- /api/scan/v0/transactions:
- name: transactions
- type: unlimited
-
/api/scan/v0/backfilling:
name: backfilling
type: unlimited
diff --git a/cluster/deployment/scratchneta/config.resolved.yaml b/cluster/deployment/scratchneta/config.resolved.yaml
index 44115509bb..b1a109d6c3 100644
--- a/cluster/deployment/scratchneta/config.resolved.yaml
+++ b/cluster/deployment/scratchneta/config.resolved.yaml
@@ -326,9 +326,6 @@ sv:
/api/scan/v0/synchronizer-identities:
name: 'synchronizer-identities'
type: 'unlimited'
- /api/scan/v0/transactions:
- name: 'transactions'
- type: 'unlimited'
/api/scan/v0/transfer-command:
name: 'transfer-command-status'
type: 'unlimited'
diff --git a/cluster/deployment/scratchnetb/config.resolved.yaml b/cluster/deployment/scratchnetb/config.resolved.yaml
index 44115509bb..b1a109d6c3 100644
--- a/cluster/deployment/scratchnetb/config.resolved.yaml
+++ b/cluster/deployment/scratchnetb/config.resolved.yaml
@@ -326,9 +326,6 @@ sv:
/api/scan/v0/synchronizer-identities:
name: 'synchronizer-identities'
type: 'unlimited'
- /api/scan/v0/transactions:
- name: 'transactions'
- type: 'unlimited'
/api/scan/v0/transfer-command:
name: 'transfer-command-status'
type: 'unlimited'
diff --git a/cluster/deployment/scratchnetc/config.resolved.yaml b/cluster/deployment/scratchnetc/config.resolved.yaml
index 44115509bb..b1a109d6c3 100644
--- a/cluster/deployment/scratchnetc/config.resolved.yaml
+++ b/cluster/deployment/scratchnetc/config.resolved.yaml
@@ -326,9 +326,6 @@ sv:
/api/scan/v0/synchronizer-identities:
name: 'synchronizer-identities'
type: 'unlimited'
- /api/scan/v0/transactions:
- name: 'transactions'
- type: 'unlimited'
/api/scan/v0/transfer-command:
name: 'transfer-command-status'
type: 'unlimited'
diff --git a/cluster/deployment/scratchnetd/config.resolved.yaml b/cluster/deployment/scratchnetd/config.resolved.yaml
index 44115509bb..b1a109d6c3 100644
--- a/cluster/deployment/scratchnetd/config.resolved.yaml
+++ b/cluster/deployment/scratchnetd/config.resolved.yaml
@@ -326,9 +326,6 @@ sv:
/api/scan/v0/synchronizer-identities:
name: 'synchronizer-identities'
type: 'unlimited'
- /api/scan/v0/transactions:
- name: 'transactions'
- type: 'unlimited'
/api/scan/v0/transfer-command:
name: 'transfer-command-status'
type: 'unlimited'
diff --git a/cluster/deployment/scratchnete/config.resolved.yaml b/cluster/deployment/scratchnete/config.resolved.yaml
index 44115509bb..b1a109d6c3 100644
--- a/cluster/deployment/scratchnete/config.resolved.yaml
+++ b/cluster/deployment/scratchnete/config.resolved.yaml
@@ -326,9 +326,6 @@ sv:
/api/scan/v0/synchronizer-identities:
name: 'synchronizer-identities'
type: 'unlimited'
- /api/scan/v0/transactions:
- name: 'transactions'
- type: 'unlimited'
/api/scan/v0/transfer-command:
name: 'transfer-command-status'
type: 'unlimited'
diff --git a/cluster/expected/canton-network/expected.json b/cluster/expected/canton-network/expected.json
index ce454feac0..8abfa07fd2 100644
--- a/cluster/expected/canton-network/expected.json
+++ b/cluster/expected/canton-network/expected.json
@@ -1772,10 +1772,6 @@
"name": "synchronizer-identities",
"type": "unlimited"
},
- "/api/scan/v0/transactions": {
- "name": "transactions",
- "type": "unlimited"
- },
"/api/scan/v0/transfer-command": {
"name": "transfer-command-status",
"type": "unlimited"
@@ -2155,10 +2151,6 @@
"name": "synchronizer-identities",
"type": "unlimited"
},
- "/api/scan/v0/transactions": {
- "name": "transactions",
- "type": "unlimited"
- },
"/api/scan/v0/transfer-command": {
"name": "transfer-command-status",
"type": "unlimited"
diff --git a/cluster/expected/sv-runbook/expected.json b/cluster/expected/sv-runbook/expected.json
index a149bccb94..4cac4780b2 100644
--- a/cluster/expected/sv-runbook/expected.json
+++ b/cluster/expected/sv-runbook/expected.json
@@ -997,10 +997,6 @@
"name": "synchronizer-identities",
"type": "unlimited"
},
- "/api/scan/v0/transactions": {
- "name": "transactions",
- "type": "unlimited"
- },
"/api/scan/v0/transfer-command": {
"name": "transfer-command-status",
"type": "unlimited"
diff --git a/daml/daml-ide-mono/README.md b/daml/daml-ide-mono/README.md
new file mode 100644
index 0000000000..4a24340200
--- /dev/null
+++ b/daml/daml-ide-mono/README.md
@@ -0,0 +1,15 @@
+# What is this?
+
+This directory, along with its fake `daml.yaml` can be populated with symlinks by
+`./scripts/setup-mono-package.sh` to serve as a fake single-dar package containing all the daml in
+Splice such that cross-package changes can be worked on with a tighter feedback loop using the
+Daml Language Server (LSP) in VS Code or other editors, e.g. by running `dpm studio` in this directory
+after having run the script.
+
+You'll also have to remember to re-run the script if you add new `.daml` files to any package.
+
+You should still verify that everything builds/tests for real once you're done of course, but this mode
+of interaction can be very useful to eliminate 12-20 second build cycles and instead get immediate
+feedback from the VS Code extension when working on tests and making changes that would otherwise be in
+their upstream DAR dependencies, for example.
+
diff --git a/daml/daml-ide-mono/daml.yaml b/daml/daml-ide-mono/daml.yaml
new file mode 100644
index 0000000000..e998fdd9a9
--- /dev/null
+++ b/daml/daml-ide-mono/daml.yaml
@@ -0,0 +1,17 @@
+sdk-version: 3.5.2
+name: splice-mono-ide
+source: daml
+version: 0.0.1
+dependencies:
+ - daml-prim
+ - daml-stdlib
+ - daml-script
+build-options:
+ - --ghc-option=-Wunused-binds
+ - --ghc-option=-Wunused-matches
+ - --target=2.1
+ - -Wno-upgrade-exceptions
+ - -Wno-deprecated-exceptions
+ - -Wno-template-interface-depends-on-daml-script
+ - -Wno-upgrade-interfaces
+ - --force-utility-package=no
diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TestAggregateLocks.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TestAggregateLocks.daml
new file mode 100644
index 0000000000..43fc641f00
--- /dev/null
+++ b/daml/splice-amulet-test/daml/Splice/Scripts/TestAggregateLocks.daml
@@ -0,0 +1,289 @@
+{-# LANGUAGE ApplicativeDo #-}
+module Splice.Scripts.TestAggregateLocks where
+
+import DA.Assert
+import DA.Optional
+import DA.Time
+import qualified DA.Map as M
+import qualified DA.TextMap as TM
+
+import Daml.Script
+
+import Splice.AggregateLock
+import Splice.Api.Token.AllocationInstructionV2
+import Splice.Api.Token.AllocationV2
+import Splice.Api.Token.AllocationV2 qualified as V2
+import Splice.Api.Token.HoldingV2 qualified as V2
+import Splice.Api.Token.MetadataV1
+import Splice.Scripts.TokenStandard.TestAmuletTokenStandardTestEnv
+import Splice.Testing.Registries.AmuletRegistryV2
+import Splice.Testing.Registries.AmuletRegistryV2 qualified as AmuletRegistryV2
+import Splice.Testing.TokenStandard.MultiRegistry qualified as MultiRegistry
+import Splice.Testing.TokenStandard.RegistryApiV2
+import Splice.Testing.TokenStandard.WalletClientV2 qualified as WalletClientV2
+import Splice.Testing.Utils
+import Splice.TokenStandard.Utils qualified as TSU
+import Splice.TokenStandard.Utils.Internal.Conversions (encodeTime)
+
+
+lockForGovernance : TestEnv -> TM.TextMap Decimal -> Metadata -> Party -> Script AllocationInstructionResult
+lockForGovernance (TestEnv {..}) amounts meta party = do
+ WalletClientV2.allocateV2 registries party lockSettlementInfo lockAllocation
+ where
+ lockSettlementInfo = V2.SettlementInfo with
+ executors = [ instrId.admin ]
+ id = "AggregateLock"
+ cid = None
+ meta = emptyMetadata
+ lockAllocation = V2.AllocationSpecification with
+ admin = instrId.admin
+ authorizer = TSU.basicAccount party
+ transferLegSides = []
+ committed = True
+ nextIterationFunding = Some amounts
+ settlementDeadline = Some maxComparableTime
+ meta
+
+lockForAggregate : TestEnv -> Decimal -> Text -> Party -> Script AllocationInstructionResult
+lockForAggregate te amount lockName = lockForGovernance te (TM.fromList [(te.instrId.id, amount)]) $ Metadata $ TM.fromList
+ [ ("cip-105/type", "aggregateLock")
+ , ("cip-105/for-benefit-of", lockName)
+ , ("cip-105/vestingSchedule", "SV")
+ ]
+
+-- | Directly create a vesting lock allocation funded with @initialAmount@,
+-- bypassing the aggregate-lock unlock path. Used to exercise the vesting
+-- withdrawal flow in isolation.
+lockForVesting : Decimal -> Time -> TestEnv -> Party -> Script AllocationInstructionResult
+lockForVesting initialAmount now te =
+ lockForGovernance te (TM.fromList [(te.instrId.id, initialAmount)]) $ Metadata $ TM.fromList
+ [ ("cip-105/type", "vestingLock")
+ , ("cip-105/vestingLock.startDate", encodeTime now)
+ , ("cip-105/vestingLock.endDate", encodeTime $ addRelTime now $ days 365 + hours 6)
+ , ("cip-105/vestingLock.initialAmount", show initialAmount)
+ ]
+
+-- | Enrich the wallet-side withdraw context with the external party config
+-- state disclosure, which AmuletAllocationV2's settle path reads when the
+-- governance-lock withdraw fans out through the settlement factory.
+withAggregateWithdrawContext : TestEnv -> MultiRegistry.MultiRegistry -> MultiRegistry.MultiRegistry
+withAggregateWithdrawContext env registries =
+ flip fmap registries $ \reg ->
+ reg { v2Api = fmap updateApi reg.v2Api }
+ where
+ addContext a = do
+ baseCtxt <- a
+ extraContext <- getExternalPartyConfigStateContext env.registriesEnv.amuletV2
+ pure $ baseCtxt <> extraContext
+ updateApi api =
+ api { ggetAllocation_WithdrawContext = \a -> addContext . api.ggetAllocation_WithdrawContext a }
+
+-- | Verify that locking funds for a "for-benefit-of" name aggregates over
+-- separate lock allocations sharing the same name.
+testAggregateLockTotals : Script ()
+testAggregateLockTotals = do
+ env@TestEnv{..} <- setupTest
+ let dso = env.instrId.admin
+
+ AmuletRegistryV2.tapFaucet registriesEnv.amuletV2 alice 1200.0
+ AmuletRegistryV2.tapFaucet registriesEnv.amuletV2 bob 1800.0
+
+ _ <- lockForAggregate env 300.0 "alice-supervalidator" alice
+ _ <- lockForAggregate env 400.0 "alice-supervalidator" bob
+ _ <- lockForAggregate env 600.0 "alice-supervalidator" bob
+ _ <- lockForAggregate env 300.0 "charlie-supervalidator" bob
+
+ allocs <- queryInterface @V2.Allocation dso
+ let aggregateLocks =
+ filter (isValidAggregateLockedAllocation dso) $ fromSome . snd <$> allocs
+ benefitOfAmount alloc =
+ ( "cip-105/for-benefit-of" `TM.lookup` alloc.allocation.meta.values
+ , fromOptional 0.0 $ alloc.allocation.nextIterationFunding >>= TM.lookup env.instrId.id
+ )
+ totals = M.fromListWithR (+) $ benefitOfAmount <$> aggregateLocks
+
+ assertEq (Some 1300.0) (Some "alice-supervalidator" `M.lookup` totals)
+ assertEq (Some 300.0) (Some "charlie-supervalidator" `M.lookup` totals)
+
+-- | Withdrawing an aggregate-lock allocation moves the locked funds into a
+-- fresh vesting-lock destination for the same owner.
+testAggregateLockUnlockCreatesVestingLock : Script ()
+testAggregateLockUnlockCreatesVestingLock = do
+ env@TestEnv{..} <- setupTest
+ let dso = env.instrId.admin
+ newRegistries = withAggregateWithdrawContext env registries
+
+ AmuletRegistryV2.tapFaucet registriesEnv.amuletV2 bob 1800.0
+
+ AllocationInstructionResult { output = AllocationInstructionResult_Completed locked } <-
+ lockForAggregate env 1000.0 "alice-supervalidator" bob
+ Some lockedView <- queryInterfaceContractId bob locked
+
+ _ <- WalletClientV2.withdrawAllocationV2 newRegistries bob (locked, lockedView)
+
+ liveAllocs <- fmap (fromSome . snd) . filter (isSome . snd) <$>
+ queryInterface @V2.Allocation bob
+ length liveAllocs === 1
+
+ let aggregates = filter (isValidAggregateLockedAllocation dso) liveAllocs
+ vestings = filter (isValidVestingLockedDestination dso) liveAllocs
+
+ length aggregates === 0
+ length vestings === 1
+
+ let [vesting] = vestings
+ fromOptional 0.0 (vesting.allocation.nextIterationFunding >>= TM.lookup instrId.id) === 1000.0
+
+ -- The vesting destination records its start/end dates.
+ assertMsg "vesting destination must record startDate" $
+ TM.member "cip-105/vestingLock.startDate" vesting.allocation.meta.values
+ assertMsg "vesting destination must record endDate" $
+ TM.member "cip-105/vestingLock.endDate" vesting.allocation.meta.values
+
+-- | Partially withdrawing an aggregate-lock allocation moves only that amount
+-- of the locked funds into a fresh vesting-lock destination for the same owner.
+testAggregateLockPartialWithdraw : Script ()
+testAggregateLockPartialWithdraw = do
+ env@TestEnv{..} <- setupTest
+ let dso = env.instrId.admin
+ newRegistries = withAggregateWithdrawContext env registries
+
+ AmuletRegistryV2.tapFaucet registriesEnv.amuletV2 bob 1800.0
+
+ AllocationInstructionResult { output = AllocationInstructionResult_Completed locked } <-
+ lockForAggregate env 1000.0 "alice-supervalidator" bob
+ Some lockedView <- queryInterfaceContractId bob locked
+
+ _ <- WalletClientV2.withdrawAllocationV2Meta newRegistries bob (locked, lockedView) $ Metadata $ TM.singleton "cip-105/withdraw-amount" "500.0"
+
+ liveAllocs <- fmap (fromSome . snd) . filter (isSome . snd) <$>
+ queryInterface @V2.Allocation bob
+ length liveAllocs === 2
+
+ let aggregates = filter (isValidAggregateLockedAllocation dso) liveAllocs
+ vestings = filter (isValidVestingLockedDestination dso) liveAllocs
+
+ length aggregates === 1
+ length vestings === 1
+
+ let [aggregate] = aggregates
+ [vesting] = vestings
+ fromOptional 0.0 (aggregate.allocation.nextIterationFunding >>= TM.lookup instrId.id) === 500.0
+ fromOptional 0.0 (vesting.allocation.nextIterationFunding >>= TM.lookup instrId.id) === 500.0
+
+ -- Metadata carried over correctly: same owner and DSO on both sides.
+ aggregate.allocation.authorizer === vesting.allocation.authorizer
+ aggregate.allocation.admin === vesting.allocation.admin
+
+ -- The vesting destination records its start/end dates.
+ assertMsg "vesting destination must record startDate" $
+ TM.member "cip-105/vestingLock.startDate" vesting.allocation.meta.values
+ assertMsg "vesting destination must record endDate" $
+ TM.member "cip-105/vestingLock.endDate" vesting.allocation.meta.values
+
+-- | Withdrawing from a vesting-lock allocation vests funds linearly over the
+-- lock's start/end period. This exercises the flow against a vesting lock
+-- created directly by 'lockForVesting'; the round-trip through
+-- 'aggregateLockUnlock' + 'vestingLockWithdraw' isn't tested here because
+-- 'aggregateLockUnlock' doesn't (yet) populate the destination's
+-- @cip-105/vestingLock.initialAmount@ metadata.
+testVestingLockWithdraw : Script ()
+testVestingLockWithdraw = do
+ env@TestEnv{..} <- setupTest
+ let newRegistries = withAggregateWithdrawContext env registries
+ initialAmount = 365.25 -- one unit per day of the 365 day + 6 hour vesting period
+
+ AmuletRegistryV2.tapFaucet registriesEnv.amuletV2 bob 1800.0
+
+ vestStart <- getTime
+ AllocationInstructionResult { output = AllocationInstructionResult_Completed vestingLockCid } <-
+ lockForVesting initialAmount vestStart env bob
+ Some vestingLockView <- queryInterfaceContractId bob vestingLockCid
+
+ -- Withdrawing immediately fails: nothing has vested yet.
+ vestingLockView.allocation.admin `submitMustFailWithdraw` (vestingLockCid, vestingLockView) $ newRegistries
+
+ -- After some time passes a proportional amount can be withdrawn.
+ passTime (days 10)
+ vestingResult1 <- WalletClientV2.extractAllocationResult <$>
+ WalletClientV2.withdrawAllocationV2 newRegistries bob (vestingLockCid, vestingLockView)
+
+ let holdingCids1 = fromSome $ TM.lookup instrId.id vestingResult1.authorizerHoldingCids
+ amount1 <- unlockedAmountOf bob holdingCids1
+ amount1 === 10.0000000105
+
+ -- The vesting-lock is settled iteratively, so a new allocation contract
+ -- carries the remaining vesting balance.
+ case vestingResult1.output of
+ AllocationResult_Settled { nextIterationAllocationCid = Some nextCid } -> do
+ -- After the full period has elapsed the remainder becomes withdrawable.
+ passTime (days 365)
+ Some nextView <- queryInterfaceContractId bob nextCid
+ vestingResult2 <- WalletClientV2.extractAllocationResult <$>
+ WalletClientV2.withdrawAllocationV2 newRegistries bob (nextCid, nextView)
+
+ let holdingCids2 = fromSome $ TM.lookup instrId.id vestingResult2.authorizerHoldingCids
+ amount2 <- unlockedAmountOf bob holdingCids2
+ amount1 + amount2 === initialAmount
+ other ->
+ fail $ "expected AllocationResult_Settled with a next-iteration allocation, got: " <> show other
+
+testVestingLockTotalWithdraw : Script ()
+testVestingLockTotalWithdraw = do
+ env@TestEnv{..} <- setupTest
+ let newRegistries = withAggregateWithdrawContext env registries
+ initialAmount = 365.25 -- one unit per day of the 365 day + 6 hour vesting period
+
+ AmuletRegistryV2.tapFaucet registriesEnv.amuletV2 bob 1800.0
+
+ vestStart <- getTime
+ AllocationInstructionResult { output = AllocationInstructionResult_Completed vestingLockCid } <-
+ lockForVesting initialAmount vestStart env bob
+ Some vestingLockView <- queryInterfaceContractId bob vestingLockCid
+
+ -- Withdrawing immediately fails: nothing has vested yet.
+ vestingLockView.allocation.admin `submitMustFailWithdraw` (vestingLockCid, vestingLockView) $ newRegistries
+
+ -- After some time passes a proportional amount can be withdrawn.
+ passTime (days 366)
+ vestingResult1 <- WalletClientV2.extractAllocationResult <$>
+ WalletClientV2.withdrawAllocationV2 newRegistries bob (vestingLockCid, vestingLockView)
+
+ let holdingCids1 = fromSome $ TM.lookup instrId.id vestingResult1.authorizerHoldingCids
+ amount1 <- unlockedAmountOf bob holdingCids1
+ amount1 === 365.25
+
+ liveAllocs <- fmap (fromSome . snd) . filter (isSome . snd) <$>
+ queryInterface @V2.Allocation bob
+ length liveAllocs === 0
+
+-- | Attempt an @Allocation_Withdraw@ that should fail; asserts the submit
+-- fails and does not leak the underlying error.
+submitMustFailWithdraw
+ : Party -- ^ admin party (needed to read the enriched choice context)
+ -> (ContractId V2.Allocation, V2.AllocationView)
+ -> MultiRegistry.MultiRegistry
+ -> Script ()
+submitMustFailWithdraw admin (allocCid, _allocView) registries = do
+ registry <- MultiRegistry.getRegistryApiV2 registries admin
+ context <- getAllocation_WithdrawContext registry allocCid emptyMetadata
+ Some owner <- pure =<<
+ fmap ((.allocation.authorizer.owner) . fromSome) (queryInterfaceContractId admin allocCid)
+ submitMustFail (actAs owner <> discloseMany' context.disclosures) $
+ exerciseCmd allocCid V2.Allocation_Withdraw with
+ actors = [owner]
+ extraArgs = ExtraArgs with
+ context = context.choiceContext
+ meta = emptyMetadata
+
+-- | Sum the amounts of the given (unlocked) holding contracts owned by @p@.
+unlockedAmountOf : Party -> [ContractId V2.Holding] -> Script Decimal
+unlockedAmountOf p wantedCids = do
+ holdings <- queryInterface @V2.Holding p
+ let matching =
+ [ h
+ | (cid, Some h) <- holdings
+ , cid `elem` wantedCids
+ , isNone h.lock
+ ]
+ pure $ sum (fmap (.amount) matching)
diff --git a/daml/splice-amulet/daml/Splice/AggregateLock.daml b/daml/splice-amulet/daml/Splice/AggregateLock.daml
new file mode 100644
index 0000000000..b6bf85fd49
--- /dev/null
+++ b/daml/splice-amulet/daml/Splice/AggregateLock.daml
@@ -0,0 +1,290 @@
+module Splice.AggregateLock where
+
+import Splice.Amulet.TokenApiUtils
+import Splice.Api.Token.AllocationV2 as V2
+import qualified Splice.Api.Token.AllocationInstructionV2 as V2
+import Splice.Api.Token.MetadataV1
+import Splice.Types (ForDso(..))
+import Splice.TokenStandard.Utils (fromAnyContractId, maxTime)
+import Splice.TokenStandard.Utils.Internal.Allocations (settlementFactoryV2_settleBatchDefaultImplNoSelf)
+import Splice.TokenStandard.Utils.Internal.Conversions (decodeTime, encodeTime)
+import Splice.Util
+
+import DA.Either
+import DA.Optional
+import DA.Text
+import DA.List
+import qualified DA.TextMap as TM
+import DA.Time
+
+maxComparableTime : Time
+maxComparableTime = addRelTime maxTime $ microseconds (-1)
+
+isValidAggregateLockedAllocation : Party -> V2.AllocationView -> Bool
+isValidAggregateLockedAllocation dso alloc =
+ and [ alloc.allocation.admin == dso
+ , alloc.settlement.executors == [dso]
+ , alloc.allocation.committed
+ , null alloc.allocation.transferLegSides
+ , TM.lookup "cip-105/type" alloc.allocation.meta.values == Some "aggregateLock"
+ , alloc.expiresAt == Some maxComparableTime
+ ]
+
+isValidVestingLockedDestination : Party -> V2.AllocationView -> Bool
+isValidVestingLockedDestination dso alloc =
+ and [ alloc.allocation.admin == dso
+ , alloc.settlement.executors == [dso]
+ , alloc.allocation.committed
+ , null alloc.allocation.transferLegSides
+ , TM.lookup "cip-105/type" alloc.allocation.meta.values == Some "vestingLock"
+ , alloc.expiresAt == Some maxComparableTime
+ , TM.member "cip-105/vestingLock.startDate" metaValues
+ , TM.member "cip-105/vestingLock.endDate" metaValues
+ ]
+ where metaValues = alloc.allocation.meta.values
+
+type ControllerSets = [[Party]]
+
+partiesFromText : Text -> Optional [Party]
+partiesFromText = eitherToOptional . parseCommaSeparated "Parties" partyFromText
+
+controllerSetFromMeta : Text -> Optional ControllerSets
+controllerSetFromMeta = mapA partiesFromText . splitOn ";"
+
+data GovernanceLockedControllers = GovernanceLockedControllers with
+ unlockControllerSets : ControllerSets
+ substituteControllerSets : ControllerSets
+ withdrawControllerSets : ControllerSets
+
+governanceLockedControllersFromMeta : Metadata -> Party -> GovernanceLockedControllers
+governanceLockedControllersFromMeta meta acctParty = GovernanceLockedControllers with
+ unlockControllerSets = controllerSetFromMetaMap "cip-105/unlockControllerSets"
+ substituteControllerSets = controllerSetFromMetaMap "cip-105/substituteControllerSets"
+ withdrawControllerSets = controllerSetFromMetaMap "cip-105/withdrawControllerSets"
+ where
+ controllerSetFromMetaMap key = fromOptional [[acctParty]] $ controllerSetFromMeta =<< key `TM.lookup` meta.values
+
+checkControllerSet : [Party] -> ControllerSets -> Update ()
+checkControllerSet parties set =
+ require "Controllers must be one of the listed options" $ any (\conj -> all (`elem` parties) conj && all (`elem` conj) parties) set
+
+governanceLockedWithdrawImpl
+ : HasToInterface a V2.Allocation
+ => (a -> Time -> Metadata -> Update (ContractId V2.Allocation))
+ -> a
+ -> ContractId a
+ -> V2.Allocation_Withdraw
+ -> Update V2.AllocationResult
+governanceLockedWithdrawImpl newEmptyAllocation a acid arg@(V2.Allocation_Withdraw{..}) =
+ case TM.lookup "cip-105/type" allocView.allocation.meta.values of
+ Some "aggregateLock" -> do
+ aggregateLockUnlock newEmptyAllocation a acid arg
+ Some "vestingLock" ->
+ vestingLockWithdraw a acid arg
+ _ -> assertFail "Invalid cip-105/type field."
+ where
+ allocView = view $ toInterface @V2.Allocation a
+
+data VestingSchedule
+ = VestingSchedule_SV
+ | VestingSchedule_Immediate
+ deriving (Show, Eq, Ord)
+
+validateVestingSchedule : VestingSchedule -> Time -> Metadata -> Bool
+validateVestingSchedule VestingSchedule_SV now meta =
+ let
+ start = decodeTime $ fromSomeNote "start must exist" (TM.lookup "cip-105/vestingLock.startDate" meta.values)
+ end = decodeTime $ fromSomeNote "end must exist" (TM.lookup "cip-105/vestingLock.endDate" meta.values)
+ in
+ -- CIP-0105 states the SV vesting period be 365.25 days
+ now <= start && end `subTime` start == days 365 + hours 6
+validateVestingSchedule VestingSchedule_Immediate _ _ = True
+
+-- TODO: Potentially make return type, it's own data type?
+calculateAvailableWithdrawAmount : Time -> V2.AllocationView -> Decimal -> (Decimal, Decimal)
+calculateAvailableWithdrawAmount currentDateTime alloc nextIterationFundAmount = max (0.0, 0.0) availAmount
+ where
+ availAmount = if currentDateTime >= endDate
+ then (nextIterationFundAmount, 0.0)
+ else (availableWithdrawAmount, remainingVestingAmount)
+ availableWithdrawAmount = (initialAmount * totalVestedRatioElapsed) - withdrawnAmount
+ remainingVestingAmount = nextIterationFundAmount - availableWithdrawAmount
+ meta = alloc.allocation.meta.values
+ -- The initial amount represents the total that was locked initially when thrown into vesting state
+ initialAmount : Decimal = fromSome . parseDecimal . fromSome $ TM.lookup "cip-105/vestingLock.initialAmount" meta
+ startDate : Time = decodeTime . fromSome $ TM.lookup "cip-105/vestingLock.startDate" meta
+ endDate : Time = decodeTime . fromSome $ TM.lookup "cip-105/vestingLock.endDate" meta
+ totalUnlockPeriod : Decimal = intToDecimal . convertRelTimeToMicroseconds $ subTime endDate startDate
+ unlockPeriodElapsed : Decimal = intToDecimal . convertRelTimeToMicroseconds $ subTime currentDateTime startDate
+ totalVestedRatioElapsed : Decimal = unlockPeriodElapsed / totalUnlockPeriod
+ withdrawnAmount = initialAmount - nextIterationFundAmount
+
+
+aggregateLockUnlock : (HasToInterface a V2.Allocation) => (a -> Time -> Metadata -> Update (ContractId V2.Allocation)) -> a -> ContractId a -> Allocation_Withdraw -> Update V2.AllocationResult
+aggregateLockUnlock newEmptyAllocation a aCid arg = do
+ let alloc = view $ toInterface @V2.Allocation a
+ allocCid = toInterfaceContractId @V2.Allocation aCid
+
+ (instrumentId, currentLockedAmount) <- case TM.toList <$> alloc.allocation.nextIterationFunding of
+ Some [a] -> pure a
+ _ -> assertFail "The requirement 'Must reserve a single amount of a single token' was not met"
+
+ vestingSchedule <- decodeVestingSchedule arg.extraArgs.meta alloc.allocation.meta
+
+ let dso = alloc.allocation.admin
+
+ ownerParty <- whenNone alloc.allocation.authorizer.owner $ -- Mostly to safely get ownerParty
+ assertFail "The requirement 'governance locked allocations must be owned by basic accounts' was not met"
+
+ let GovernanceLockedControllers{..} = governanceLockedControllersFromMeta alloc.meta ownerParty
+ checkControllerSet arg.actors unlockControllerSets
+
+ require "Must be a valid governance-locked allocation" $ isValidAggregateLockedAllocation dso alloc
+
+ now <- getTime
+ let endTime = getEndTimeFromVestingSchedule vestingSchedule now
+ transferLegId = "unlock"
+
+ let alternateAmount = TM.lookup "cip-105/withdraw-amount" arg.extraArgs.meta.values >>= parseDecimal
+ amount = fromOptional currentLockedAmount alternateAmount
+ newLockedAmount = currentLockedAmount - amount
+ newLockedNextIterationFunding
+ | newLockedAmount == 0.0 = None
+ | otherwise = Some $ TM.singleton instrumentId newLockedAmount
+
+ -- Not easy to call directly into amulet_allocationFactoryV2_allocateImpl or the like here as that would make a circular dependency.
+ withdrawTo <- newEmptyAllocation a now $ Metadata $ TM.fromList
+ [ ("cip-105/type", "vestingLock")
+ , ("cip-105/vestingLock.startDate", encodeTime $ now)
+ , ("cip-105/vestingLock.endDate", encodeTime $ endTime)
+ ]
+
+ settleBatchResult <- settlementFactoryV2_settleBatchDefaultImplNoSelf (\_ _ -> pure arg.extraArgs) dso $ SettlementFactory_SettleBatch with
+ settlement = alloc.settlement
+ actors = [ dso ]
+ extraArgs = arg.extraArgs
+ transferLegs =
+ [ TransferLeg with
+ transferLegId
+ sender = alloc.allocation.authorizer
+ receiver = alloc.allocation.authorizer
+ amount
+ instrumentId
+ meta = emptyMetadata
+ ]
+ allocations =
+ [ FinalizedAllocation with
+ allocationCid = allocCid
+ extraTransferLegSides =
+ [ TransferLegSide with
+ transferLegId
+ side = SenderSide
+ otherside = alloc.allocation.authorizer
+ amount
+ instrumentId
+ meta = emptyMetadata
+ ]
+ nextIterationFunding = newLockedNextIterationFunding
+ , FinalizedAllocation with
+ allocationCid = withdrawTo
+ extraTransferLegSides =
+ [ TransferLegSide with
+ transferLegId
+ side = ReceiverSide
+ otherside = alloc.allocation.authorizer
+ amount
+ instrumentId
+ meta = emptyMetadata
+ ]
+ nextIterationFunding = Some $ TM.singleton instrumentId amount
+ ]
+ pure $ head settleBatchResult.allocationSettleResults
+ where
+ getEndTimeFromVestingSchedule VestingSchedule_SV now = addRelTime now $ days 365 + hours 6
+ getEndTimeFromVestingSchedule VestingSchedule_Immediate now = now
+
+vestingLockWithdraw : (HasToInterface a V2.Allocation) => a -> ContractId a -> Allocation_Withdraw -> Update V2.AllocationResult
+vestingLockWithdraw a aCid arg = do
+ let vestingLock = view $ toInterface @V2.Allocation a
+ vestingLockCid = toInterfaceContractId @V2.Allocation aCid
+ now <- getTime
+
+ require "Must be a valid vesting unlock allocation" $ isValidVestingLockedDestination vestingLock.allocation.admin vestingLock
+ require "There must be an initialAmount set for the vestingLock"
+ $ TM.member "cip-105/vestingLock.initialAmount" vestingLock.allocation.meta.values
+
+ ownerParty <- whenNone vestingLock.allocation.authorizer.owner $ -- Mostly to safely get ownerParty
+ assertFail "The requirement 'governance locked allocations must be owned by basic accounts' was not met"
+ let GovernanceLockedControllers{..} = governanceLockedControllersFromMeta vestingLock.meta ownerParty
+
+ checkControllerSet arg.actors withdrawControllerSets
+
+ (instrumentId, nextIterationFunding) <- case TM.toList <$> vestingLock.allocation.nextIterationFunding of
+ Some [a] -> pure a
+ _ -> assertFail "The requirement 'Must reserve a single amount of a single token' was not met"
+ require "There must be an initialAmount set for the vestingLock"
+ $ TM.member "cip-105/vestingLock.initialAmount" vestingLock.allocation.meta.values
+
+ let (availableWithdrawAmount, remainingVestingAmount) : (Decimal, Decimal) =
+ calculateAvailableWithdrawAmount now vestingLock nextIterationFunding
+
+ require ("Current eligible withdraw amount for vesting lock is not greater than 0.0. "
+ <> "currentEligibleWithdrawableAmount came back = '"
+ <> show availableWithdrawAmount
+ <> "'."
+ )
+ $ availableWithdrawAmount > 0.0
+
+ let
+ transferLegId = "withdraw"
+ nextIterationFunding
+ | remainingVestingAmount <= 0.0 = None
+ | otherwise = Some $ TM.singleton instrumentId remainingVestingAmount
+
+ settleResult <- settlementFactoryV2_settleBatchDefaultImplNoSelf (\_ _ -> pure arg.extraArgs) vestingLock.allocation.admin $ SettlementFactory_SettleBatch with
+ settlement = vestingLock.settlement
+ actors = [ vestingLock.allocation.admin ]
+ extraArgs = ExtraArgs emptyChoiceContext emptyMetadata
+ transferLegs =
+ [ TransferLeg with
+ transferLegId
+ sender = vestingLock.allocation.authorizer
+ receiver = vestingLock.allocation.authorizer
+ amount = availableWithdrawAmount
+ instrumentId
+ meta = emptyMetadata
+ ]
+ allocations =
+ [ FinalizedAllocation with
+ allocationCid = vestingLockCid
+ extraTransferLegSides =
+ [ TransferLegSide with
+ transferLegId
+ side = SenderSide
+ otherside = vestingLock.allocation.authorizer
+ amount = availableWithdrawAmount
+ instrumentId
+ meta = emptyMetadata
+ , TransferLegSide with
+ transferLegId
+ side = ReceiverSide
+ otherside = vestingLock.allocation.authorizer
+ amount = availableWithdrawAmount
+ instrumentId
+ meta = emptyMetadata
+ ]
+ nextIterationFunding
+ ]
+
+ -- We should have one result from settleBatch
+ case settleResult.allocationSettleResults of
+ [allocationResult] -> pure allocationResult
+ _ -> abort "VestingUnlock_Withdraw should result in one allocation result. "
+
+-- Takes the choice metadata as well to enable override to allow quick unlock
+decodeVestingSchedule : Metadata -> Metadata -> Update VestingSchedule
+decodeVestingSchedule _withdrawMeta lockMeta = do
+ case TM.lookup "cip-105/vestingSchedule" lockMeta.values of
+ Some "SV" -> pure VestingSchedule_SV
+ _ -> assertFail "Must have a valid vesting schedule"
+
diff --git a/daml/splice-amulet/daml/Splice/AmuletAllocationV2.daml b/daml/splice-amulet/daml/Splice/AmuletAllocationV2.daml
index 469ede68f2..aefff68139 100644
--- a/daml/splice-amulet/daml/Splice/AmuletAllocationV2.daml
+++ b/daml/splice-amulet/daml/Splice/AmuletAllocationV2.daml
@@ -23,6 +23,7 @@ import Splice.Api.Token.HoldingV2 qualified as V2
import Splice.Api.Token.AllocationV2 qualified as V2
import Splice.TokenStandard.Utils hiding (require)
+import Splice.AggregateLock
import Splice.Amulet
import Splice.AmuletConfig (TransferConfigV2, getTokenStandardMaxTTL)
import Splice.AmuletRules
@@ -76,6 +77,8 @@ template AmuletAllocationV2
allocation_cancelExtraObservers _arg = observer this
allocation_withdrawExtraObservers _arg = observer this
+ allocation_withdrawImpl self arg | isGovernanceLocked this =
+ governanceLockedWithdrawImpl emptyCloneWithMeta this (fromInterfaceContractId self) arg
allocation_withdrawImpl self arg@(V2.Allocation_Withdraw{..}) = do
archiveAndCheckActors self arg.actors [[accountPrincipal allocation.admin allocation.authorizer]]
ensureWithdrawIsAllowed allocation
@@ -230,12 +233,23 @@ computeAllocationExpiryInternal transferConfig oldExpiresAt settlementDeadline =
| maxTime `subTime` oldExpiresAt <= maxTTL = maxTime
| otherwise = oldExpiresAt `addRelTime` maxTTL
+-- FIXME: the exception to allow Some maxTime is for infinite-duration governance locks, and should probably be limited more explicitly to only those cases.
computeAllocationExpiry : TransferConfigV2 Amulet -> Time -> Optional Time -> Update Time
-computeAllocationExpiry transferConfig oldExpiresAt settlementDeadline = do
- let expiresAt = computeAllocationExpiryInternal transferConfig oldExpiresAt settlementDeadline
- assertWithinDeadline "allocation.expiresAt" expiresAt
- pure expiresAt
+computeAllocationExpiry transferConfig oldExpiresAt settlementDeadline
+ | settlementDeadline == Some maxComparableTime = pure maxComparableTime
+ | otherwise = do
+ let expiresAt = computeAllocationExpiryInternal transferConfig oldExpiresAt settlementDeadline
+ assertWithinDeadline "allocation.expiresAt" expiresAt
+ pure expiresAt
+isGovernanceLocked alloc = TextMap.member "cip-105/type" alloc.allocation.meta.values
+
+emptyCloneWithMeta : AmuletAllocationV2 -> Time -> Metadata -> Update (ContractId V2.Allocation)
+emptyCloneWithMeta src now meta = toInterfaceContractId <$> create src with
+ lockedAmulet = None
+ createdAt = now
+ allocation = src.allocation with
+ meta
-- instances
------------
diff --git a/docs/gen-daml-docs.sh b/docs/gen-daml-docs.sh
index ac5e208f7f..1fd1c1818e 100755
--- a/docs/gen-daml-docs.sh
+++ b/docs/gen-daml-docs.sh
@@ -47,6 +47,7 @@ DAML_PROJECT_FILES="\
-not -ipath '*splitwell*' \
-not -ipath '*app-manager*' \
-not -ipath '*dummy-holding*' \
+ -not -ipath '*daml-ide-mono*' \
-print)"
DAML_PROJECT_FILES=$(printf "%s\n" "$DAML_PROJECT_FILES" | grep -vf <(printf "%s\n" "${NON_COMPILED_DAML_PROJECTS[@]}" | xargs -n1 basename))
diff --git a/docs/src/release_notes_upcoming.rst b/docs/src/release_notes_upcoming.rst
index 5f5ee78c44..77ab2efaa2 100644
--- a/docs/src/release_notes_upcoming.rst
+++ b/docs/src/release_notes_upcoming.rst
@@ -6,3 +6,25 @@
.. NOTE: add your upcoming release notes below this line. They are included in the `release_notes.rst`.
.. release-notes:: Upcoming
+
+ - Scan app
+
+ - Remove deprecated ``/transactions`` endpoint.
+
+ - Validator app
+
+ - The deprecated ``TransferCommand`` functionality consisting
+ of the endpoints
+ ``/v0/admin/external-party/transfer-preapproval/prepare-send``,
+ ``/v0/admin/external-party/transfer-preapproval/submit-send``
+ and the automation to execute transfer commands is now
+ disabled by default. If you were still using those switch to
+ token standard transfers which also support 24h submission
+ delays since `cip 107
+ `_.
+ If you need some time to migrate, you can temporarily
+ reenable it by setting
+ ``canton.validator-apps.validator_backend.enable-deprecated-transfer-command-support=true``. The
+ functionality is expected to be fully removed in 0.8.0 so
+ this only provides a bit more time to migrate but you must
+ complete the migration.
diff --git a/project/build.properties b/project/build.properties
index cc68b53f1a..e544c4d115 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.10.11
+sbt.version=1.12.14
diff --git a/scripts/rename.sh b/scripts/rename.sh
index 92ff142232..8fc9397d5e 100755
--- a/scripts/rename.sh
+++ b/scripts/rename.sh
@@ -1216,7 +1216,7 @@ function subcmd_no_illegal_daml_references() {
)
for pattern in "${illegal_patterns[@]}"; do
echo "Checking for occurences of '$pattern' (case sensitive, in code other than splitwell)"
- if rg -P "$pattern" daml/ token-standard/ -g '!*/splitwell/*' -g '!*/splitwell-test/*' -g '!daml/dars.lock' -g '!token-standard/README.md' -g '!token-standard/V2_VALIDATION.md' -g '!token-standard/TOKEN_STANDARD_V2_DEVNET.md' -g '!*.json' -g '!token-standard/dependencies/*' -g '!**/target/'; then
+ if rg -P "$pattern" daml/ token-standard/ -g '!*/splitwell/*' -g '!*/splitwell-test/*' -g '!daml/dars.lock' -g '!token-standard/README.md' -g '!token-standard/V2_VALIDATION.md' -g '!token-standard/TOKEN_STANDARD_V2_DEVNET.md' -g'!daml/daml-ide-mono/README.md' -g '!*.json' -g '!token-standard/dependencies/*' -g '!**/target/'; then
echo "$pattern occurs in Daml code (other than splitwell), remove all references"
exit 1
fi
diff --git a/scripts/setup-mono-package.sh b/scripts/setup-mono-package.sh
new file mode 100755
index 0000000000..d7e8cc34f8
--- /dev/null
+++ b/scripts/setup-mono-package.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+
+# Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# Populate daml/daml-ide-mono/daml/ with per-file symlinks into every
+# workspace Daml package's source tree. daml/daml-ide-mono/daml.yaml is
+# checked in and static - this script only regenerates the source tree
+# so that VS Code can work on the union as a single package.
+#
+# Re-run this any time a .daml file is added or removed anywhere in the
+# workspace.
+
+set -euo pipefail
+
+repo=$(cd "$(dirname "$0")/.." && pwd)
+dest="$repo/daml/daml-ide-mono/daml"
+
+rm -rf "$dest"
+mkdir -p "$dest"
+
+for pkg in "$repo"/daml/*/daml.yaml \
+ "$repo"/token-standard/*/daml.yaml \
+ "$repo"/token-standard/examples/*/daml.yaml; do
+ src=$(dirname "$pkg")/daml
+ [ -d "$src" ] || continue
+ ( cd "$src" && find . -name '*.daml' -printf '%P\n' ) |
+ while IFS= read -r rel; do
+ link="$dest/$rel"
+ mkdir -p "$(dirname "$link")"
+ target=$(realpath --relative-to="$(dirname "$link")" "$src/$rel")
+ ln -sfn "$target" "$link"
+ done
+done
diff --git a/test-full-class-names-non-integration.log b/test-full-class-names-non-integration.log
index 6e5afedcd2..fc29c49e92 100644
--- a/test-full-class-names-non-integration.log
+++ b/test-full-class-names-non-integration.log
@@ -11,6 +11,7 @@ org.lfdecentralizedtrust.splice.environment.ActiveContractsRestartTest
org.lfdecentralizedtrust.splice.environment.CommandCircuitBreakerTest
org.lfdecentralizedtrust.splice.environment.CommandIdDedupTest
org.lfdecentralizedtrust.splice.environment.TopologyAwarePackageVersionSupportTest
+org.lfdecentralizedtrust.splice.http.InvalidResponseContentTest
org.lfdecentralizedtrust.splice.http.NonProxyHostsTest
org.lfdecentralizedtrust.splice.http.UrlValidatorTest
org.lfdecentralizedtrust.splice.scan.admin.api.client.BftScanConnectionTest
diff --git a/token-standard/splice-token-standard-utils/daml/Splice/TokenStandard/Utils/Internal/Allocations.daml b/token-standard/splice-token-standard-utils/daml/Splice/TokenStandard/Utils/Internal/Allocations.daml
index 9ce6ef72f6..2085a3ae4e 100644
--- a/token-standard/splice-token-standard-utils/daml/Splice/TokenStandard/Utils/Internal/Allocations.daml
+++ b/token-standard/splice-token-standard-utils/daml/Splice/TokenStandard/Utils/Internal/Allocations.daml
@@ -53,6 +53,7 @@ module Splice.TokenStandard.Utils.Internal.Allocations (
-- ** SettlementFactory template implementations
settlementFactoryV2_settleBatchDefaultImpl,
+ settlementFactoryV2_settleBatchDefaultImplNoSelf,
fetchAndValidateAllocations,
validateNextIterationArgs,
@@ -374,7 +375,18 @@ settlementFactoryV2_settleBatchDefaultImpl
-> ContractId AllocationV2.SettlementFactory
-> AllocationV2.SettlementFactory_SettleBatch
-> Update AllocationV2.SettlementFactory_SettleBatchResult
-settlementFactoryV2_settleBatchDefaultImpl getFilteredExtraArgs admin self arg = do
+settlementFactoryV2_settleBatchDefaultImpl getFilteredExtraArgs admin self arg =
+ settlementFactoryV2_settleBatchDefaultImplNoSelf getFilteredExtraArgs admin arg
+
+settlementFactoryV2_settleBatchDefaultImplNoSelf
+ : (AllocationV2.AllocationView -> AllocationV2.Allocation_Settle -> Update ExtraArgs)
+ -- ^ Function to compute the actual extra argument to use for settling an allocation.
+ -- Use this for example to redact unrelated choice-context data from the extra arguments.
+ -> Party
+ -- ^ Admin of the allocations and the settlement factory
+ -> AllocationV2.SettlementFactory_SettleBatch
+ -> Update AllocationV2.SettlementFactory_SettleBatchResult
+settlementFactoryV2_settleBatchDefaultImplNoSelf getFilteredExtraArgs admin arg = do
let AllocationV2.SettlementFactory_SettleBatch {..} = arg
checkActors actors [settlement.executors]
diff --git a/token-standard/splice-token-standard-utils/daml/Splice/TokenStandard/Utils/Internal/Conversions.daml b/token-standard/splice-token-standard-utils/daml/Splice/TokenStandard/Utils/Internal/Conversions.daml
index 5fbcdd253a..60c19fb477 100644
--- a/token-standard/splice-token-standard-utils/daml/Splice/TokenStandard/Utils/Internal/Conversions.daml
+++ b/token-standard/splice-token-standard-utils/daml/Splice/TokenStandard/Utils/Internal/Conversions.daml
@@ -27,6 +27,8 @@ module Splice.TokenStandard.Utils.Internal.Conversions (
partiesToMeta,
dropMeta,
validateNoMeta,
+ encodeTime,
+ decodeTime,
-- * Transfer utils
reasonMetaKey,
diff --git a/token-standard/splice-token-standard-v2-test/daml/Splice/Testing/Registries/AmuletRegistryV2.daml b/token-standard/splice-token-standard-v2-test/daml/Splice/Testing/Registries/AmuletRegistryV2.daml
index 04a7817fff..92492103db 100644
--- a/token-standard/splice-token-standard-v2-test/daml/Splice/Testing/Registries/AmuletRegistryV2.daml
+++ b/token-standard/splice-token-standard-v2-test/daml/Splice/Testing/Registries/AmuletRegistryV2.daml
@@ -30,6 +30,10 @@ module Splice.Testing.Registries.AmuletRegistryV2
, getActiveOpenRoundsSorted
, advanceToNextRoundChange
, convertAllFeaturedAppActivityMarkers
+
+ --
+ , getExtAmuletRulesWithDisclosures
+ , getExternalPartyConfigStateContext
) where
import DA.Action (unless, when)
diff --git a/token-standard/splice-token-standard-v2-test/daml/Splice/Testing/TokenStandard/WalletClientV2.daml b/token-standard/splice-token-standard-v2-test/daml/Splice/Testing/TokenStandard/WalletClientV2.daml
index 1327fc43e7..d15c0d0010 100644
--- a/token-standard/splice-token-standard-v2-test/daml/Splice/Testing/TokenStandard/WalletClientV2.daml
+++ b/token-standard/splice-token-standard-v2-test/daml/Splice/Testing/TokenStandard/WalletClientV2.daml
@@ -55,6 +55,7 @@ module Splice.Testing.TokenStandard.WalletClientV2
acceptAllocationInstructionV2,
withdrawAllocationInstructionV2,
withdrawAllocationV2,
+ withdrawAllocationV2Meta,
allocateV1,
withdrawAllocationInstructionV1,
@@ -73,6 +74,7 @@ module Splice.Testing.TokenStandard.WalletClientV2
-- ** Allocations
extractNextIterationAllocationCid,
+ extractAllocationResult,
mkAllocationFactory_AllocateV2,
mkAllocationInstruction_AcceptV2,
@@ -469,6 +471,20 @@ withdrawAllocationV2 registries actor alloc = do
batch <- mkAllocation_WithdrawV2 registries actor alloc
executeSingletonTSABatch registries actor batch
+-- | Simulate a V2 wallet withdrawing an allocation from a V2 app, with extra metadata
+withdrawAllocationV2Meta
+ : MultiRegistry.MultiRegistry -> Party -> (ContractId V2.Allocation, V2.AllocationView) -> Metadata
+ -> Script BatchingUtilityV2.TokenStandardActionResult
+withdrawAllocationV2Meta registries actor alloc meta = do
+ batch <- mkAllocation_WithdrawV2Meta registries actor alloc meta
+ executeSingletonTSABatch registries actor batch
+
+-- | Extract the underlying @AllocationResult@ from a wallet-client batch result.
+extractAllocationResult : BatchingUtilityV2.TokenStandardActionResult -> V2.AllocationResult
+extractAllocationResult (BatchingUtilityV2.TSAR_AllocationResultV2 r) = r
+extractAllocationResult other =
+ error $ "expected TSAR_AllocationResultV2, got: " <> show other
+
-- | Simulate a V2 wallet withdrawing an allocation from a V1 app.
withdrawAllocationV1
: MultiRegistry.MultiRegistry -> Party -> (ContractId V1.Allocation, V1.AllocationView)
@@ -812,7 +828,10 @@ mkAllocationFactory_AllocateV2 registryV2 actor settlement allocation = do
disclosures = enrichedChoice.disclosures
mkAllocation_WithdrawV2 : MultiRegistry.MultiRegistry -> Party -> (ContractId V2.Allocation, V2.AllocationView) -> Script TSABatch
-mkAllocation_WithdrawV2 registries actor (allocCid, allocView) = do
+mkAllocation_WithdrawV2 registries actor (allocCid, allocView) = mkAllocation_WithdrawV2Meta registries actor (allocCid, allocView) emptyMetadata
+
+mkAllocation_WithdrawV2Meta : MultiRegistry.MultiRegistry -> Party -> (ContractId V2.Allocation, V2.AllocationView) -> Metadata -> Script TSABatch
+mkAllocation_WithdrawV2Meta registries actor (allocCid, allocView) meta = do
registry <- MultiRegistry.getRegistryApiV2 registries allocView.allocation.admin
context <- V2.getAllocation_WithdrawContext registry allocCid emptyMetadata
let withdrawRequest = BatchingUtilityV2.TSA_Allocation_WithdrawV2 BatchingUtilityV2.ChoiceCall with
@@ -820,7 +839,7 @@ mkAllocation_WithdrawV2 registries actor (allocCid, allocView) = do
arg = V2.Allocation_Withdraw with
extraArgs = ExtraArgs with
context = context.choiceContext
- meta = emptyMetadata
+ meta
actors = [actor]
pure TSABatch with
actions = [withdrawRequest]