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]