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 9f9083697c..2097f429c8 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 @@ -83,6 +83,12 @@ abstract class SvAppReference( httpCommand(HttpSvPublicAppClient.getSvOnboardingStatus(candidate)) } + @Help.Summary("Buy member traffic for a new validator (DevNet only) (via client API)") + def devNetBuyMemberTraffic(participantId: ParticipantId): Unit = + consoleEnvironment.run { + httpCommand(HttpSvPublicAppClient.DevNetBuyMemberTraffic(participantId.toProtoPrimitive)) + } + @Help.Summary("Prepare a validator onboarding and return an onboarding secret (via client API)") def devNetOnboardValidatorPrepare(): String = consoleEnvironment.run { diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/PermissionedSynchronizerIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/PermissionedSynchronizerIntegrationTest.scala index e59d2efaa5..2f0a5abc4c 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/PermissionedSynchronizerIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/PermissionedSynchronizerIntegrationTest.scala @@ -100,5 +100,25 @@ class PermissionedSynchronizerIntegrationTest aliceValidatorBackend.onboardUser("TestUser") }, ) + + clue("Sponser SV Buys Member Traffic for Bob in the DevNet") { + sv1Backend.devNetBuyMemberTraffic(bobValidatorBackend.participantClient.id) + } + + clue("Verify Bob is granted ParticipantSynchronizerPermission") { + eventually() { + sv1ScanBackend.getParticipantSynchronizerPermission( + decentralizedSynchronizerId.toProtoPrimitive, + bobValidatorBackend.participantClient.id.toProtoPrimitive, + ) shouldBe Some( + SynchronizerPermissionState(None) + ) + } + } + + clue("Bob Validator starts and onboards correctly") { + bobValidatorBackend.startSync() + bobValidatorBackend.onboardUser("TestUserBob") + } } } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/performance/tests/SvDsoStoreIngestionPerformanceTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/performance/tests/SvDsoStoreIngestionPerformanceTest.scala index a07405c35a..6a5588dc02 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/performance/tests/SvDsoStoreIngestionPerformanceTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/performance/tests/SvDsoStoreIngestionPerformanceTest.scala @@ -64,7 +64,6 @@ class SvDsoStoreIngestionPerformanceTest( participantId = mkParticipantId("IngestionPerformanceIngestionTest"), IngestionConfig(), defaultLimit = HardLimit.tryCreate(Limit.DefaultMaxPageSize), - config = None, )(ec, templateJsonDecoder, closeContext).multiDomainAcsStore } diff --git a/apps/sv/src/main/openapi/sv-internal.yaml b/apps/sv/src/main/openapi/sv-internal.yaml index f20abd07ef..2280b01486 100644 --- a/apps/sv/src/main/openapi/sv-internal.yaml +++ b/apps/sv/src/main/openapi/sv-internal.yaml @@ -493,6 +493,31 @@ paths: "$ref": "#/components/schemas/OnboardSvSequencerResponse" "400": $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/400" + /v0/devnet/onboard/validator/purchase-traffic: + post: + tags: [sv] + x-jvm-package: sv_public + description: "faucet for validator candidates to buy member traffic" + operationId: "devNetBuyMemberTraffic" + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/DevNetBuyMemberTrafficRequest" + responses: + "200": + description: ok + content: + text/plain: + schema: + type: string + "400": + $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/400" + "500": + $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/500" + "501": + $ref: "../../../../common/src/main/openapi/common-external.yaml#/components/responses/501" /v0/devnet/onboard/validator/prepare: post: tags: [sv] @@ -721,6 +746,14 @@ components: Human-readable alias of the validator party decoded from the stored secret. type: string + DevNetBuyMemberTrafficRequest: + type: object + required: + - participant_id + properties: + participant_id: + type: string + PrepareValidatorOnboardingRequest: type: object required: 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 3de5e74742..e90c91e4bc 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 @@ -117,8 +117,11 @@ class SvApp( metrics, ) { - override def packagesForJsonDecoding: Seq[DarResource] = - super.packagesForJsonDecoding ++ DarResources.dsoGovernance.all ++ DarResources.validatorLifecycle.all ++ DarResources.amuletNameService.all + override def packagesForJsonDecoding: Seq[DarResource] = { + val base = + super.packagesForJsonDecoding ++ DarResources.dsoGovernance.all ++ DarResources.validatorLifecycle.all ++ DarResources.amuletNameService.all + if (config.permissionedSynchronizer) base ++ DarResources.wallet.all else base + } override def preInitializeBeforeLedgerConnection()(implicit tc: TraceContext): Future[Unit] = { val participantAdminConnection = new ParticipantAdminConnection( diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvPublicAppClient.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvPublicAppClient.scala index 5111c3b7f0..378e93d8e9 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvPublicAppClient.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/api/client/commands/HttpSvPublicAppClient.scala @@ -158,6 +158,28 @@ object HttpSvPublicAppClient { } } + case class DevNetBuyMemberTraffic(participantId: String) + extends BaseCommandPublic[http.DevNetBuyMemberTrafficResponse, Unit] { + + override def submitRequest( + client: Client, + headers: List[HttpHeader], + ): EitherT[Future, Either[ + Throwable, + HttpResponse, + ], http.DevNetBuyMemberTrafficResponse] = + client.devNetBuyMemberTraffic( + body = definitions.DevNetBuyMemberTrafficRequest(participantId), + headers = headers, + ) + + override def handleOk()(implicit + decoder: TemplateJsonDecoder + ) = { case http.DevNetBuyMemberTrafficResponse.OK(_) => + Right(()) + } + } + case class DevNetOnboardValidatorPrepare() extends BaseCommandPublic[http.DevNetOnboardValidatorPrepareResponse, String] { diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvPublicHandler.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvPublicHandler.scala index 381a2a6e08..c684e0f450 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvPublicHandler.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/admin/http/HttpSvPublicHandler.scala @@ -307,6 +307,40 @@ class HttpSvPublicHandler( } } + /** Intended use: Used by validator candidates to buy member traffic using SV's DevNet faucet + * + * Protection: Rate limiting, endpoint only used for DevNet + */ + override def devNetBuyMemberTraffic( + respond: r0.DevNetBuyMemberTrafficResponse.type + )( + body: definitions.DevNetBuyMemberTrafficRequest + )(extracted: TraceContext): Future[r0.DevNetBuyMemberTrafficResponse] = { + implicit val traceContext: TraceContext = extracted + withSpan(s"$workflowId.devNetBuyMemberTraffic") { _ => _ => + if (isDevNet && config.permissionedSynchronizer) { + for { + participantId <- ParticipantId.fromProtoPrimitive( + body.participantId, + "participant_id", + ) match { + case Right(id) => Future.successful(id) + case Left(err) => + Future.failed(HttpErrorHandler.badRequest(s"Invalid participant ID: $err")) + } + _ <- devNetTapAndBuyMemberTraffic(participantId) + + } yield r0.DevNetBuyMemberTrafficResponseOK("Success") + } else { + Future.failed( + HttpErrorHandler.notImplemented( + "Traffic purchasing self-service is only available in DevNet when permissioned synchronizer is enabled." + ) + ) + } + } + } + /** Intended use: Used by other validators to get a free onboarding secret * * Protection: Rate limiting, endpoint only used for DevNet @@ -850,6 +884,55 @@ class HttpSvPublicHandler( .yieldUnit() } yield () + private def devNetTapAndBuyMemberTraffic( + participantId: ParticipantId + )(implicit tc: TraceContext): Future[Unit] = { + for { + dsoRules <- dsoStore.getDsoRules() + + svWalletInstall <- retryProvider.retryForClientCalls( + "wait_for_wallet_install", + "Wait for SV WalletAppInstall contract to be ingested", + for { + svWalletInstallOpt <- svStoreWithIngestion.store.lookupWalletAppInstallByEndUser(svParty) + install <- svWalletInstallOpt match { + case Some(install) => Future.successful(install) + case None => + Future.failed( + HttpErrorHandler.internalServerError( + "SV WalletAppInstall contract not found." + ) + ) + } + } yield install, + logger, + ) + + cmd = svWalletInstall.contractId.exerciseWalletAppInstall_CreateBuyTrafficRequest( + participantId.toProtoPrimitive, + dsoRules.payload.config.decentralizedSynchronizer.activeSynchronizerId, + dsoStore.domainMigrationId.toInt, + config.devNetPublicSetupTrafficAmount, + clock.now.plus(java.time.Duration.ofMinutes(5)).toInstant, + s"devnet-onboard-${participantId.toProtoPrimitive}-${clock.now.toInstant.toEpochMilli}", + ) + + _ = logger.info(s"Creating BuyTrafficRequest by $svUserName for $participantId") + + _ <- dsoStoreWithIngestion + .connection(SpliceLedgerConnectionPriority.Medium) + .submit( + actAs = Seq(svParty), + readAs = Seq(dsoParty), + update = cmd, + ) + .withSynchronizerId(dsoRules.domain) + .noDedup + .yieldUnit() + + } yield () + } + private def startSvOnboarding( candidateName: String, candidateParty: PartyId, diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/config/SvAppConfig.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/config/SvAppConfig.scala index 98b413b339..b8e8dee1f5 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/config/SvAppConfig.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/config/SvAppConfig.scala @@ -485,6 +485,7 @@ case class SvAppBackendConfig( // where intentional overlap is required. instanceLockEnabled: Boolean = true, minMemberTrafficToOnboardValidator: Long = 100000L, + devNetPublicSetupTrafficAmount: Long = 10000000L, ) extends SpliceBackendConfig { def allIgnoredAmuletVersions: Set[String] = diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/NodeInitializerUtil.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/NodeInitializerUtil.scala index e10edf4164..8dc2867e28 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/NodeInitializerUtil.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/onboarding/NodeInitializerUtil.scala @@ -147,7 +147,6 @@ trait NodeInitializerUtil extends NamedLogging with Spanning with SynchronizerNo config.automation.ingestion, config.parameters.defaultLimit, acsStoreDescriptorUserVersion, - Some(config), ) } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvDsoStore.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvDsoStore.scala index c5cb3d90e0..821f631f43 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvDsoStore.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvDsoStore.scala @@ -68,7 +68,6 @@ trait SvDsoStore with ActiveVotesStore { protected val outerLoggerFactory: NamedLoggerFactory protected def templateJsonDecoder: TemplateJsonDecoder - def config: Option[org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig] override protected lazy val loggerFactory: NamedLoggerFactory = outerLoggerFactory.append("store", "dsoParty") @@ -80,7 +79,6 @@ trait SvDsoStore SvDsoStore.contractFilter( key.dsoParty, domainMigrationId, - config.map(_.permissionedSynchronizer).getOrElse(false), ) def key: SvStore.Key @@ -1212,7 +1210,6 @@ object SvDsoStore { ingestionConfig: IngestionConfig, defaultLimit: Limit, acsStoreDescriptorUserVersion: Option[Long] = None, - svBackendconfig: Option[org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig], )(implicit ec: ExecutionContext, templateJsonDecoder: TemplateJsonDecoder, @@ -1228,7 +1225,6 @@ object SvDsoStore { ingestionConfig, acsStoreDescriptorUserVersion, defaultLimit = defaultLimit, - config = svBackendconfig, ) } @@ -1236,7 +1232,6 @@ object SvDsoStore { def contractFilter( dsoParty: PartyId, domainMigrationId: Long, - enablePermissionedSynchronizer: Boolean, ): MultiDomainAcsStore.ContractFilter[ DsoAcsStoreRowData, AcsInterfaceViewRowData.NoInterfacesIngested, @@ -1672,22 +1667,23 @@ object SvDsoStore { contractExpiresAt = Some(Timestamp.assertFromInstant(contract.payload.expiresAt)), ) }, + mkFilter(vl.ValidatorLicenseRequest.COMPANION)( + req => req.payload.dso == dso, + versionGuard = { case (pkgVersionSupport, now) => + (tc) => pkgVersionSupport.supportsPermissionedSynchronizer(Seq(dsoParty), now)(tc) + }, + ) { contract => + DsoAcsStoreRowData( + contract, + contractExpiresAt = Some(Timestamp.assertFromInstant(contract.payload.expiresAt)), + validator = Some(PartyId.tryFromProtoPrimitive(contract.payload.validator)), + ) + }, ) - val finalFilters = if (enablePermissionedSynchronizer) { - dsoFilters + mkFilter(vl.ValidatorLicenseRequest.COMPANION)(req => req.payload.dso == dso) { - contract => - DsoAcsStoreRowData( - contract, - contractExpiresAt = Some(Timestamp.assertFromInstant(contract.payload.expiresAt)), - validator = Some(PartyId.tryFromProtoPrimitive(contract.payload.validator)), - ) - } - } else dsoFilters - MultiDomainAcsStore.SimpleContractFilter( dsoParty, - finalFilters, + dsoFilters, ) } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvSvStore.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvSvStore.scala index 4345d88999..c5966f7366 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvSvStore.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvSvStore.scala @@ -19,8 +19,9 @@ import org.lfdecentralizedtrust.splice.util.{Contract, TemplateJsonDecoder} import com.digitalasset.canton.lifecycle.CloseContext import com.digitalasset.canton.logging.NamedLoggerFactory import com.digitalasset.canton.resource.DbStorage -import com.digitalasset.canton.topology.ParticipantId +import com.digitalasset.canton.topology.{ParticipantId, PartyId} import com.digitalasset.canton.tracing.TraceContext +import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.install.WalletAppInstall import org.lfdecentralizedtrust.splice.config.IngestionConfig import org.lfdecentralizedtrust.splice.store.db.AcsInterfaceViewRowData @@ -53,6 +54,19 @@ trait SvSvStore extends AppStore { ] = lookupValidatorOnboardingBySecretWithOffset(secret).map(_.value) + def lookupWalletAppInstallByEndUserWithOffset( + endUserParty: PartyId + )(implicit tc: TraceContext): Future[ + QueryResult[Option[Contract[WalletAppInstall.ContractId, WalletAppInstall]]] + ] + + def lookupWalletAppInstallByEndUser( + endUserParty: PartyId + )(implicit tc: TraceContext): Future[ + Option[Contract[WalletAppInstall.ContractId, WalletAppInstall]] + ] = + lookupWalletAppInstallByEndUserWithOffset(endUserParty).map(_.value) + def lookupUsedSecretWithOffset( secret: String )(implicit tc: TraceContext): Future[ @@ -121,37 +135,51 @@ object SvSvStore { ) /** Contract filter of an sv acs store for a specific acs party. */ - def contractFilter(key: SvStore.Key): MultiDomainAcsStore.ContractFilter[ + def contractFilter( + key: SvStore.Key + ): MultiDomainAcsStore.ContractFilter[ SvAcsStoreRowData, AcsInterfaceViewRowData.NoInterfacesIngested, ] = { import MultiDomainAcsStore.mkFilter val sv = key.svParty.toProtoPrimitive + val svFilters = Map( + mkFilter(vo.ValidatorOnboarding.COMPANION)(co => co.payload.sv == sv) { contract => + SvAcsStoreRowData( + contract, + contractExpiresAt = Some(Timestamp.assertFromInstant(contract.payload.expiresAt)), + onboardingSecret = Some(contract.payload.candidateSecret), + ) + }, + mkFilter(vo.UsedSecret.COMPANION)(co => co.payload.sv == sv) { contract => + SvAcsStoreRowData( + contract, + onboardingSecret = Some(contract.payload.secret), + ) + }, + mkFilter(so.SvOnboardingConfirmed.COMPANION)(co => co.payload.svParty == sv) { contract => + SvAcsStoreRowData( + contract, + contractExpiresAt = Some(Timestamp.assertFromInstant(contract.payload.expiresAt)), + svCandidateName = Some(contract.payload.svName), + ) + }, + mkFilter(WalletAppInstall.COMPANION)( + co => co.payload.endUserParty == sv, + versionGuard = { case (pkgVersionSupport, now) => + (tc) => pkgVersionSupport.supportsPermissionedSynchronizer(Seq(key.dsoParty), now)(tc) + }, + ) { contract => + SvAcsStoreRowData( + contract + ) + }, + ) + MultiDomainAcsStore.SimpleContractFilter( key.svParty, - Map( - mkFilter(vo.ValidatorOnboarding.COMPANION)(co => co.payload.sv == sv) { contract => - SvAcsStoreRowData( - contract, - contractExpiresAt = Some(Timestamp.assertFromInstant(contract.payload.expiresAt)), - onboardingSecret = Some(contract.payload.candidateSecret), - ) - }, - mkFilter(vo.UsedSecret.COMPANION)(co => co.payload.sv == sv) { contract => - SvAcsStoreRowData( - contract, - onboardingSecret = Some(contract.payload.secret), - ) - }, - mkFilter(so.SvOnboardingConfirmed.COMPANION)(co => co.payload.svParty == sv) { contract => - SvAcsStoreRowData( - contract, - contractExpiresAt = Some(Timestamp.assertFromInstant(contract.payload.expiresAt)), - svCandidateName = Some(contract.payload.svName), - ) - }, - ), + svFilters, ) } } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvDsoStore.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvDsoStore.scala index 7ad4dd8b20..b9d0bffed8 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvDsoStore.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvDsoStore.scala @@ -84,7 +84,6 @@ class DbSvDsoStore( ingestionConfig: IngestionConfig, acsStoreDescriptorUserVersion: Option[Long] = None, override val defaultLimit: Limit, - override val config: Option[org.lfdecentralizedtrust.splice.sv.config.SvAppBackendConfig], )(implicit override protected val ec: ExecutionContext, override protected val templateJsonDecoder: TemplateJsonDecoder, diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvSvStore.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvSvStore.scala index 61157a5b51..33892810f1 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvSvStore.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/db/DbSvSvStore.scala @@ -17,7 +17,7 @@ import org.lfdecentralizedtrust.splice.util.{Contract, TemplateJsonDecoder} import com.digitalasset.canton.lifecycle.CloseContext import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging} import com.digitalasset.canton.resource.DbStorage -import com.digitalasset.canton.topology.ParticipantId +import com.digitalasset.canton.topology.{ParticipantId, PartyId} import com.digitalasset.canton.tracing.TraceContext import org.lfdecentralizedtrust.splice.config.IngestionConfig import org.lfdecentralizedtrust.splice.store.db.AcsQueries.AcsStoreId @@ -104,6 +104,33 @@ class DbSvSvStore( )).getOrRaise(offsetExpectedError()) } + import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.install.WalletAppInstall + + override def lookupWalletAppInstallByEndUserWithOffset( + endUserParty: PartyId + )(implicit tc: TraceContext): Future[MultiDomainAcsStore.QueryResult[ + Option[Contract[WalletAppInstall.ContractId, WalletAppInstall]] + ]] = waitUntilAcsIngested { + (for { + resultWithOffset <- storage + .querySingle( + selectFromAcsTableWithOffset( + DbSvSvStore.tableName, + acsStoreId, + domainMigrationId, + WalletAppInstall.COMPANION, + where = sql""" + create_arguments->>'endUserParty' = ${lengthLimited(endUserParty.toProtoPrimitive)} + """, + ).headOption, + "lookupWalletAppInstallByEndUserWithOffset", + ) + } yield QueryResult( + resultWithOffset.offset, + resultWithOffset.row.map(contractFromRow(WalletAppInstall.COMPANION)(_)), + )).getOrRaise(offsetExpectedError()) + } + override def lookupUsedSecretWithOffset(secret: String)(implicit tc: TraceContext ): Future[MultiDomainAcsStore.QueryResult[Option[Contract[UsedSecret.ContractId, UsedSecret]]]] = diff --git a/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvDsoStoreTest.scala b/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvDsoStoreTest.scala index 593c47839a..518289caa2 100644 --- a/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvDsoStoreTest.scala +++ b/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvDsoStoreTest.scala @@ -2349,7 +2349,6 @@ class DbSvDsoStoreTest participantId = mkParticipantId("SvDsoStoreTest"), IngestionConfig(), defaultLimit = HardLimit.tryCreate(Limit.DefaultMaxPageSize), - config = None, )(parallelExecutionContext, implicitly, implicitly) for { _ <- store.multiDomainAcsStore.testIngestionSink.initialize() diff --git a/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvSvStoreTest.scala b/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvSvStoreTest.scala index d9881f151d..d4162be43a 100644 --- a/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvSvStoreTest.scala +++ b/apps/sv/src/test/scala/org/lfdecentralizedtrust/splice/store/db/SvSvStoreTest.scala @@ -12,8 +12,10 @@ import org.lfdecentralizedtrust.splice.util.{ResourceTemplateDecoder, TemplateJs import com.digitalasset.canton.concurrent.FutureSupervisor import com.digitalasset.canton.lifecycle.FutureUnlessShutdown import com.digitalasset.canton.resource.DbStorage +import com.digitalasset.canton.topology.PartyId import com.digitalasset.canton.tracing.TraceContext import com.digitalasset.canton.{HasActorSystem, HasExecutionContext, SynchronizerAlias} +import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.install.WalletAppInstall import org.lfdecentralizedtrust.splice.config.IngestionConfig import java.time.Instant @@ -104,6 +106,51 @@ abstract class SvSvStoreTest extends StoreTestBase with HasExecutionContext { } } + "lookupWalletAppInstallByEndUserWithOffset" should { + + "find a WalletAppInstall for the SV party and ignore others" in { + val endUser1 = storeSvParty + val endUser2 = PartyId.tryFromProtoPrimitive("endUser2::domain") + + val wanted = walletAppInstall(endUser1, "sv-user") + val unwanted = walletAppInstall(endUser2, "other-user") + + val firstOffset = 101L + val secondOffset = 202L + + for { + store <- mkStore() + _ <- dummyDomain.create( + wanted, + firstOffset, + createdEventSignatories = Seq(storeSvParty), + )(store.multiDomainAcsStore) + + _ <- dummyDomain.create( + unwanted, + secondOffset, + createdEventSignatories = Seq(storeSvParty), + )(store.multiDomainAcsStore) + } yield { + store.lookupWalletAppInstallByEndUserWithOffset(endUser1).futureValue should be( + QueryResult(secondOffset, Some(wanted)) + ) + store.lookupWalletAppInstallByEndUserWithOffset(endUser2).futureValue should be( + QueryResult(secondOffset, None) // we ingest only when co.payload.endUserParty == sv + ) + } + } + + "return just the offset if there's no entries" in { + val nobody = PartyId.tryFromProtoPrimitive("nobody::domain") + for { + store <- mkStore() + result <- store.lookupWalletAppInstallByEndUserWithOffset(nobody) + } yield result should be(QueryResult(acsOffset, None)) + } + + } + "find a UsedSecret by secret in JSON format" in { val wanted = usedSecret( """{"sv": "splice-client-1::dummy", "validator_party_hint": "splice-client-2", "secret": "good_secret"}""" @@ -124,6 +171,23 @@ abstract class SvSvStoreTest extends StoreTestBase with HasExecutionContext { } } + private def walletAppInstall(endUserParty: PartyId, endUserName: String) = { + val template = new WalletAppInstall( + dsoParty.toProtoPrimitive, + storeSvParty.toProtoPrimitive, + endUserName, + endUserParty.toProtoPrimitive, + ) + + val templateId = WalletAppInstall.TEMPLATE_ID_WITH_PACKAGE_ID + + contract( + templateId, + new WalletAppInstall.ContractId(nextCid()), + template, + ) + } + private def validatorOnboarding(secret: String) = { val template = new vo.ValidatorOnboarding( @@ -173,7 +237,8 @@ class DbSvSvStoreTest DarResources.amulet.all ++ DarResources.validatorLifecycle.all ++ DarResources.dsoGovernance.all ++ - DarResources.dsoGovernance.all + DarResources.dsoGovernance.all ++ + DarResources.wallet.all ) implicit val templateJsonDecoder: TemplateJsonDecoder = new ResourceTemplateDecoder(packageSignatures, loggerFactory) @@ -187,7 +252,10 @@ class DbSvSvStoreTest participantId = mkParticipantId("SvSvStoreTest"), IngestionConfig(), defaultLimit = HardLimit.tryCreate(Limit.DefaultMaxPageSize), - )(parallelExecutionContext, implicitly, implicitly) + )(parallelExecutionContext, implicitly, implicitly) { + override lazy val acsContractFilter = + SvSvStore.contractFilter(key) + } for { _ <- store.multiDomainAcsStore.testIngestionSink.initialize() _ <- store.multiDomainAcsStore.testIngestionSink