Skip to content

Commit 1fbd5a8

Browse files
Allow creating an allocation with user-specified parameters (#1082)
--------- Signed-off-by: Oriol Muñoz <[email protected]>
1 parent 8765838 commit 1fbd5a8

File tree

21 files changed

+1337
-180
lines changed

21 files changed

+1337
-180
lines changed

apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/WalletAppReference.scala

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.{
1414
}
1515
import org.lfdecentralizedtrust.splice.environment.SpliceConsoleEnvironment
1616
import org.lfdecentralizedtrust.splice.http.v0.definitions.{
17+
AllocateAmuletResponse,
1718
GetBuyTrafficRequestStatusResponse,
1819
GetTransferOfferStatusResponse,
1920
TransferInstructionResultResponse,
@@ -30,7 +31,10 @@ import com.digitalasset.canton.console.Help
3031
import com.digitalasset.canton.data.CantonTimestamp
3132
import com.digitalasset.canton.topology.{PartyId, SynchronizerId}
3233
import org.lfdecentralizedtrust.splice.codegen.java.splice.amulettransferinstruction.AmuletTransferInstruction
33-
import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.transferinstructionv1
34+
import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.{
35+
allocationv1,
36+
transferinstructionv1,
37+
}
3438

3539
abstract class WalletAppReference(
3640
override val spliceConsoleEnvironment: SpliceConsoleEnvironment,
@@ -544,6 +548,16 @@ abstract class WalletAppReference(
544548
HttpWalletAppClient.TokenStandard.WithdrawTransfer(contractId)
545549
)
546550
}
551+
552+
@Help.Summary("Creates an AmuletAllocation")
553+
@Help.Description(
554+
"Create an AmuletAllocation, which is an implementation of the Token Standard allocation for Amulet."
555+
)
556+
def allocateAmulet(spec: allocationv1.AllocationSpecification): AllocateAmuletResponse = {
557+
consoleEnvironment.run {
558+
httpCommand(HttpWalletAppClient.TokenStandard.AllocateAmulet(spec))
559+
}
560+
}
547561
}
548562

549563
/** Client (aka remote) reference to a wallet app in the style of ParticipantClientReference, i.e.,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package org.lfdecentralizedtrust.splice.integration.tests
2+
3+
import com.daml.ledger.api.v2.event.CreatedEvent.toJavaProto
4+
import com.daml.ledger.javaapi.data.CreatedEvent
5+
import com.digitalasset.canton.admin.api.client.data.TemplateId
6+
import com.digitalasset.canton.topology.PartyId
7+
import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletallocation.AmuletAllocation
8+
import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.allocationv1.{
9+
AllocationSpecification,
10+
SettlementInfo,
11+
TransferLeg,
12+
Reference as SettlementReference,
13+
}
14+
import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.holdingv1.InstrumentId
15+
import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.metadatav1.Metadata
16+
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
17+
import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.SpliceTestConsoleEnvironment
18+
import org.lfdecentralizedtrust.splice.util.{
19+
Contract,
20+
FrontendLoginUtil,
21+
SpliceUtil,
22+
WalletFrontendTestUtil,
23+
WalletTestUtil,
24+
}
25+
26+
import java.time.temporal.ChronoUnit
27+
import java.time.{Instant, LocalDateTime, ZoneOffset}
28+
import java.util.Optional
29+
30+
class AllocationsFrontendIntegrationTest
31+
extends FrontendIntegrationTestWithSharedEnvironment("alice")
32+
with WalletTestUtil
33+
with WalletFrontendTestUtil
34+
with FrontendLoginUtil {
35+
36+
private val amuletPrice = 2
37+
override def walletAmuletPrice = SpliceUtil.damlDecimal(amuletPrice.toDouble)
38+
override def environmentDefinition: SpliceEnvironmentDefinition =
39+
EnvironmentDefinition
40+
.simpleTopology1Sv(this.getClass.getSimpleName)
41+
.withAmuletPrice(amuletPrice)
42+
43+
private def createAllocation(sender: PartyId)(implicit
44+
ev: SpliceTestConsoleEnvironment,
45+
webDriver: WebDriverType,
46+
) = {
47+
val validatorPartyId = aliceValidatorBackend.getValidatorPartyId()
48+
val receiver = validatorPartyId
49+
val now = LocalDateTime
50+
.now()
51+
// FE only supports minute precision, so we truncate
52+
.truncatedTo(ChronoUnit.MINUTES)
53+
.toInstant(ZoneOffset.UTC)
54+
val allocateBefore = now.plusSeconds(3600)
55+
val settleBefore = now.plusSeconds(3600 * 2)
56+
57+
def wantedAllocation(requestedAt: Instant) = new AllocationSpecification(
58+
new SettlementInfo(
59+
validatorPartyId.toProtoPrimitive,
60+
new SettlementReference("some_reference", Optional.empty),
61+
requestedAt,
62+
allocateBefore,
63+
settleBefore,
64+
new Metadata(java.util.Map.of("k1", "v1", "k2", "v2")),
65+
),
66+
"some_transfer_leg_id",
67+
new TransferLeg(
68+
sender.toProtoPrimitive,
69+
validatorPartyId.toProtoPrimitive,
70+
BigDecimal(12).bigDecimal.setScale(10),
71+
new InstrumentId(dsoParty.toProtoPrimitive, "Amulet"),
72+
new Metadata(java.util.Map.of("k3", "v3")),
73+
),
74+
)
75+
// use the data here to create, but ignore the requestedAt field
76+
val create = wantedAllocation(Instant.now())
77+
78+
actAndCheck(
79+
"go to allocations page", {
80+
click on "navlink-allocations"
81+
},
82+
)(
83+
"allocations page is shown",
84+
_ => {
85+
currentUrl should endWith("/allocations")
86+
},
87+
)
88+
89+
actAndCheck(
90+
"create allocation", {
91+
textField("create-allocation-transfer-leg-id").underlying
92+
.sendKeys(create.transferLegId)
93+
textField("create-allocation-settlement-ref-id").underlying
94+
.sendKeys(create.settlement.settlementRef.id)
95+
click on "create-allocation-transfer-leg-receiver"
96+
setAnsField(
97+
textField("create-allocation-transfer-leg-receiver"),
98+
receiver.toProtoPrimitive,
99+
receiver.toProtoPrimitive,
100+
)
101+
click on "create-allocation-settlement-executor"
102+
setAnsField(
103+
textField("create-allocation-settlement-executor"),
104+
validatorPartyId.toProtoPrimitive,
105+
validatorPartyId.toProtoPrimitive,
106+
)
107+
click on "create-allocation-amulet-amount"
108+
numberField("create-allocation-amulet-amount").value = ""
109+
numberField("create-allocation-amulet-amount").underlying.sendKeys(
110+
create.transferLeg.amount.toString
111+
)
112+
113+
setDateTime(
114+
"alice",
115+
"create-allocation-settlement-settle-before",
116+
create.settlement.settleBefore,
117+
)
118+
setDateTime(
119+
"alice",
120+
"create-allocation-settlement-allocate-before",
121+
create.settlement.allocateBefore,
122+
)
123+
124+
setMeta(create.settlement.meta, "settlement")
125+
setMeta(create.transferLeg.meta, "transfer-leg")
126+
127+
click on "create-allocation-submit-button"
128+
},
129+
)(
130+
"the allocation is created",
131+
_ => {
132+
// TODO (#1106): check in the FE as opposed to checking the ledger
133+
val allocation =
134+
aliceValidatorBackend.participantClientWithAdminToken.ledger_api.state.acs
135+
.of_party(
136+
party = sender,
137+
filterTemplates = Seq(AmuletAllocation.TEMPLATE_ID).map(TemplateId.fromJavaIdentifier),
138+
)
139+
.loneElement
140+
141+
val specification = Contract
142+
.fromCreatedEvent(AmuletAllocation.COMPANION)(
143+
CreatedEvent.fromProto(toJavaProto(allocation.event))
144+
)
145+
.getOrElse(fail(s"Failed to parse allocation contract: $allocation"))
146+
.payload
147+
.allocation
148+
149+
specification should be(wantedAllocation(specification.settlement.requestedAt))
150+
},
151+
)
152+
}
153+
154+
private def setMeta(meta: Metadata, idPrefix: String)(implicit webDriver: WebDriverType) = {
155+
import scala.jdk.CollectionConverters.*
156+
157+
meta.values.asScala.zipWithIndex.foreach { case ((key, value), index) =>
158+
click on s"$idPrefix-add-meta"
159+
textField(s"$idPrefix-meta-key-$index").underlying.sendKeys(key)
160+
textField(s"$idPrefix-meta-value-$index").underlying.sendKeys(value)
161+
}
162+
}
163+
164+
"A wallet UI" should {
165+
166+
"create a token standard allocation" in { implicit env =>
167+
val aliceDamlUser = aliceWalletClient.config.ledgerApiUser
168+
val aliceUserParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend)
169+
aliceWalletClient.tap(1000)
170+
171+
withFrontEnd("alice") { implicit webDriver =>
172+
browseToAliceWallet(aliceDamlUser)
173+
174+
createAllocation(aliceUserParty)
175+
}
176+
}
177+
178+
}
179+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package org.lfdecentralizedtrust.splice.integration.tests
2+
3+
import com.daml.ledger.api.v2.event.CreatedEvent.toJavaProto
4+
import com.daml.ledger.javaapi.data.CreatedEvent
5+
import com.digitalasset.canton.HasExecutionContext
6+
import com.digitalasset.canton.admin.api.client.data.TemplateId
7+
import com.digitalasset.canton.data.CantonTimestamp
8+
import com.digitalasset.canton.topology.PartyId
9+
import org.lfdecentralizedtrust.splice.codegen.java.splice.amuletallocation.AmuletAllocation
10+
import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.allocationv1.{
11+
AllocationSpecification,
12+
SettlementInfo,
13+
TransferLeg,
14+
Reference as SettlementReference,
15+
}
16+
import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.holdingv1.InstrumentId
17+
import org.lfdecentralizedtrust.splice.codegen.java.splice.api.token.metadatav1.Metadata
18+
import org.lfdecentralizedtrust.splice.http.v0.definitions.AllocationInstructionResultOutput.members
19+
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
20+
import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{
21+
IntegrationTestWithSharedEnvironment,
22+
SpliceTestConsoleEnvironment,
23+
}
24+
import org.lfdecentralizedtrust.splice.util.*
25+
26+
import java.util.Optional
27+
28+
class AmuletAllocationsIntegrationTest
29+
extends IntegrationTestWithSharedEnvironment
30+
with HasExecutionContext
31+
with WalletTestUtil
32+
with WalletTxLogTestUtil {
33+
34+
override def environmentDefinition: EnvironmentDefinition = {
35+
EnvironmentDefinition
36+
.simpleTopology1Sv(this.getClass.getSimpleName)
37+
}
38+
39+
private def createAllocation(sender: PartyId)(implicit
40+
ev: SpliceTestConsoleEnvironment
41+
) = {
42+
val validatorPartyId = aliceValidatorBackend.getValidatorPartyId()
43+
val receiver = validatorPartyId
44+
val now = CantonTimestamp.now()
45+
val allocateBefore = now.plusSeconds(3600)
46+
val settleBefore = now.plusSeconds(3600 * 2)
47+
def wantedAllocation(requestedAt: CantonTimestamp) = new AllocationSpecification(
48+
new SettlementInfo(
49+
validatorPartyId.toProtoPrimitive,
50+
new SettlementReference("some_reference", Optional.empty),
51+
requestedAt.toInstant,
52+
allocateBefore.toInstant,
53+
settleBefore.toInstant,
54+
new Metadata(java.util.Map.of("k1", "v1", "k2", "v2")),
55+
),
56+
"some_transfer_leg_id",
57+
new TransferLeg(
58+
sender.toProtoPrimitive,
59+
receiver.toProtoPrimitive,
60+
BigDecimal(12).bigDecimal.setScale(10),
61+
new InstrumentId(dsoParty.toProtoPrimitive, "Amulet"),
62+
new Metadata(java.util.Map.of("k3", "v3")),
63+
),
64+
)
65+
66+
actAndCheck(
67+
"create an allocation", {
68+
aliceWalletClient.allocateAmulet(wantedAllocation(now))
69+
},
70+
)(
71+
"the allocation is created",
72+
created => {
73+
inside(created.output) { case members.AllocationInstructionResultCompleted(_) =>
74+
succeed
75+
}
76+
77+
// TODO (#1106): use whatever endpoint was introduced to check
78+
val allocation =
79+
aliceValidatorBackend.participantClientWithAdminToken.ledger_api.state.acs
80+
.of_party(
81+
party = sender,
82+
filterTemplates = Seq(AmuletAllocation.TEMPLATE_ID).map(TemplateId.fromJavaIdentifier),
83+
)
84+
.loneElement
85+
86+
val specification = Contract
87+
.fromCreatedEvent(AmuletAllocation.COMPANION)(
88+
CreatedEvent.fromProto(toJavaProto(allocation.event))
89+
)
90+
.getOrElse(fail(s"Failed to parse allocation contract: $allocation"))
91+
.payload
92+
.allocation
93+
94+
specification should be(
95+
wantedAllocation(CantonTimestamp.assertFromInstant(specification.settlement.requestedAt))
96+
)
97+
},
98+
)
99+
}
100+
101+
"A wallet" should {
102+
103+
"create a token standard amulet allocation" in { implicit env =>
104+
val aliceUserParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend)
105+
aliceWalletClient.tap(1000)
106+
107+
createAllocation(aliceUserParty)
108+
}
109+
110+
}
111+
}

apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/FrontendIntegrationTest.scala

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import cats.syntax.parallel.*
55
import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.{
66
IntegrationTest,
77
IntegrationTestWithSharedEnvironment,
8-
TestCommon,
98
SpliceTestConsoleEnvironment,
9+
TestCommon,
1010
}
1111
import com.digitalasset.canton.tracing.TraceContext
1212
import com.digitalasset.canton.util.FutureInstances.*
@@ -26,18 +26,19 @@ import org.openqa.selenium.{
2626
import org.openqa.selenium.html5.WebStorage
2727
import org.openqa.selenium.json.{Json, JsonInput}
2828
import org.openqa.selenium.support.ui.{ExpectedCondition, ExpectedConditions, WebDriverWait}
29-
import org.scalatest.ParallelTestExecution
29+
import org.scalatest.{Assertion, ParallelTestExecution}
3030
import org.scalatest.matchers.{MatchResult, Matcher}
3131
import org.scalatestplus.selenium.WebBrowser
3232

3333
import java.io.{File, StringReader}
3434
import java.nio.file.Paths
3535
import java.text.SimpleDateFormat
36-
import java.time.Duration
36+
import java.time.{Duration, Instant, ZoneOffset}
3737
import java.util.Calendar
3838
import java.util.concurrent.atomic.AtomicLong
3939
import org.openqa.selenium.firefox.GeckoDriverService
4040

41+
import java.time.format.DateTimeFormatter
4142
import scala.collection.mutable
4243
import scala.concurrent.blocking
4344
import scala.concurrent.duration.*
@@ -682,6 +683,37 @@ trait FrontendTestCommon extends TestCommon with WebBrowser with CustomMatchers
682683
}
683684
clickOn(query)
684685
}
686+
687+
private val DefaultDateTimeFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
688+
def setDateTime(
689+
party: String,
690+
pickerId: String,
691+
instant: Instant,
692+
dateTimeFormat: DateTimeFormatter = DefaultDateTimeFormat,
693+
)(implicit webDriver: WebDriverType): Assertion = {
694+
setDateTime(party, pickerId, dateTimeFormat.format(instant.atOffset(ZoneOffset.UTC)))
695+
}
696+
697+
def setDateTime(party: String, pickerId: String, dateTime: String)(implicit
698+
webDriver: WebDriverType
699+
): Assertion = {
700+
clue(s"$party selects the date $dateTime") {
701+
val dateTimePicker = webDriver.findElement(By.id(pickerId))
702+
eventually() {
703+
dateTimePicker.clear()
704+
dateTimePicker.click()
705+
// Typing in the "filler" characters can mess up the input badly
706+
// Note: this breaks on Feb 29th because the date library validates that the day
707+
// of the month is valid for the year you enter and because the year is entered
708+
// one digit at a time that fails and it resets it to Feb 28th. Luckily,
709+
// this does not happen very often …
710+
dateTimePicker.sendKeys(dateTime.replaceAll("[^0-9APM]", ""))
711+
eventually()(
712+
dateTimePicker.getAttribute("value").toLowerCase shouldBe dateTime.toLowerCase
713+
)
714+
}
715+
}
716+
}
685717
}
686718

687719
object FrontendIntegrationTest {

0 commit comments

Comments
 (0)