Skip to content

Commit ab432a8

Browse files
author
DA Automation
committed
[ci] Update Splice from CCI
Signed-off-by: DA Automation <splice-maintainers@digitalasset.com>
1 parent 336837e commit ab432a8

File tree

25 files changed

+802
-88
lines changed

25 files changed

+802
-88
lines changed

apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/PackageVersionSupport.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ trait PackageVersionSupport {
102102
)
103103
}
104104

105+
def supportsDescriptionInTransferPreapprovals(parties: Seq[PartyId], now: CantonTimestamp)(
106+
implicit tc: TraceContext
107+
): Future[FeatureSupport] = {
108+
isDarSupported(
109+
parties,
110+
PackageIdResolver.Package.SpliceWallet,
111+
now,
112+
// this is when the description field was added to transfer preapprovals
113+
DarResources.wallet_0_1_9,
114+
)
115+
}
116+
105117
private def isDarSupported(
106118
parties: Seq[PartyId],
107119
packageId: PackageIdResolver.Package,

apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/ValidatorApp.scala

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,11 @@ class ValidatorApp(
746746
com.google.protobuf.duration.Duration
747747
.toJavaProto(DurationConversion.toProto(config.deduplicationDuration.asJavaApproximation))
748748
)
749+
synchronizerId <- scanConnection.getAmuletRulesDomain()(traceContext)
750+
packageVersionSupport = PackageVersionSupport.createPackageVersionSupport(
751+
synchronizerId,
752+
readOnlyLedgerConnection,
753+
)
749754
walletManagerOpt =
750755
if (config.enableWallet) {
751756
val externalPartyWalletManager = new ExternalPartyWalletManager(
@@ -777,6 +782,7 @@ class ValidatorApp(
777782
storage: Storage,
778783
retryProvider,
779784
scanConnection,
785+
packageVersionSupport,
780786
loggerFactory,
781787
domainMigrationInfo,
782788
participantId,
@@ -794,7 +800,6 @@ class ValidatorApp(
794800
logger.info("Not starting wallet as it's disabled")
795801
None
796802
}
797-
synchronizerId <- scanConnection.getAmuletRulesDomain()(traceContext)
798803
automation = new ValidatorAutomationService(
799804
config.automation,
800805
config.participantIdentitiesBackup,
@@ -830,10 +835,7 @@ class ValidatorApp(
830835
config.contactPoint,
831836
initialSynchronizerTime,
832837
loggerFactory,
833-
packageVersionSupport = PackageVersionSupport.createPackageVersionSupport(
834-
synchronizerId,
835-
readOnlyLedgerConnection,
836-
),
838+
packageVersionSupport,
837839
)
838840
_ <- config.appInstances.toList.traverse({ case (name, instance) =>
839841
appInitStep(s"Set up app instance $name") {
@@ -889,6 +891,11 @@ class ValidatorApp(
889891
loggerFactory,
890892
)
891893

894+
packageVersionSupport = PackageVersionSupport.createPackageVersionSupport(
895+
synchronizerId,
896+
readOnlyLedgerConnection,
897+
)
898+
892899
adminHandler =
893900
new HttpValidatorAdminHandler(
894901
automation,
@@ -899,17 +906,13 @@ class ValidatorApp(
899906
getAmuletRulesDomain = scanConnection.getAmuletRulesDomain,
900907
scanConnection = scanConnection,
901908
participantAdminConnection,
909+
packageVersionSupport,
902910
config,
903911
clock,
904912
retryProvider = retryProvider,
905913
loggerFactory,
906914
)
907915

908-
packageVersionSupport = PackageVersionSupport.createPackageVersionSupport(
909-
synchronizerId,
910-
readOnlyLedgerConnection,
911-
)
912-
913916
walletInternalHandler = walletManagerOpt.map(walletManager =>
914917
new HttpWalletHandler(
915918
walletManager,

apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorAdminHandler.scala

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.install.amulet
1919
import org.lfdecentralizedtrust.splice.environment.ledger.api.DedupOffset
2020
import org.lfdecentralizedtrust.splice.environment.{
2121
BaseLedgerConnection,
22+
PackageVersionSupport,
2223
ParticipantAdminConnection,
2324
RetryFor,
2425
RetryProvider,
@@ -73,6 +74,7 @@ class HttpValidatorAdminHandler(
7374
getAmuletRulesDomain: GetAmuletRulesDomain,
7475
scanConnection: ScanConnection,
7576
participantAdminConnection: ParticipantAdminConnection,
77+
packageVersionSupport: PackageVersionSupport,
7678
config: ValidatorAppBackendConfig,
7779
clock: Clock,
7880
retryProvider: RetryProvider,
@@ -761,6 +763,12 @@ class HttpValidatorAdminHandler(
761763
}
762764
}
763765
externalPartyAmuletRules <- scanConnection.getExternalPartyAmuletRules()
766+
supportsDescription <- packageVersionSupport
767+
.supportsDescriptionInTransferPreapprovals(
768+
Seq(receiverParty, senderParty, store.key.dsoParty),
769+
clock.now,
770+
)
771+
.map(_.supported)
764772
commands = externalPartyAmuletRules.toAssignedContract
765773
.getOrElse(
766774
throw Status.Code.FAILED_PRECONDITION.toStatus
@@ -777,7 +785,7 @@ class HttpValidatorAdminHandler(
777785
body.amount.bigDecimal,
778786
body.expiresAt.toInstant,
779787
body.nonce,
780-
body.description.toJava,
788+
Option.when(supportsDescription)(body.description).flatten.toJava,
781789
)
782790
)
783791
.update

apps/wallet/frontend/src/__tests__/wallet.test.tsx

Lines changed: 138 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,17 @@ const dsoEntry = nameServiceEntries.find(e => e.name.startsWith('dso'))!;
2323

2424
const walletUrl = window.splice_config.services.validator.url;
2525

26-
function featureSupportHandler(tokenStandardSupported: boolean) {
26+
function featureSupportHandler(
27+
tokenStandardSupported: boolean,
28+
transferPreapprovalDescriptionSupported: boolean
29+
) {
2730
return rest.get(`${walletUrl}/v0/feature-support`, async (_, res, ctx) => {
28-
return res(ctx.json({ token_standard: tokenStandardSupported }));
31+
return res(
32+
ctx.json({
33+
token_standard: tokenStandardSupported,
34+
transfer_preapproval_description: transferPreapprovalDescriptionSupported,
35+
})
36+
);
2937
});
3038
}
3139

@@ -100,7 +108,7 @@ describe('Wallet user can', () => {
100108
});
101109

102110
test('not see dso in list of transfer-offer receivers', async () => {
103-
server.use(featureSupportHandler(true));
111+
server.use(featureSupportHandler(true, true));
104112
const user = userEvent.setup();
105113
render(
106114
<WalletConfigProvider>
@@ -133,7 +141,7 @@ describe('Wallet user can', () => {
133141
transferTests(false);
134142

135143
test('fall back to non-token standard transfers when the token standard is not supported', async () => {
136-
server.use(featureSupportHandler(false));
144+
server.use(featureSupportHandler(false, true));
137145

138146
const user = userEvent.setup();
139147
render(
@@ -192,6 +200,121 @@ describe('Wallet user can', () => {
192200
// The withdraw has a dummy conversion rate of 0 so no amulet conversion rate is displayed
193201
expect(await screen.findAllByText('@')).toHaveLength(3);
194202
});
203+
204+
test('transfer preapproval (without token standard) does not show nor send description if not supported', async () => {
205+
// token standard as not supported
206+
server.use(featureSupportHandler(false, false));
207+
const user = userEvent.setup();
208+
render(
209+
<WalletConfigProvider>
210+
<App />
211+
</WalletConfigProvider>
212+
);
213+
expect(await screen.findByText('Transfer')).toBeDefined();
214+
215+
const transferOffersLink = screen.getByRole('link', { name: 'Transfer' });
216+
await user.click(transferOffersLink);
217+
expect(screen.getByRole('heading', { name: 'Transfers' })).toBeDefined();
218+
219+
const receiverInput = screen
220+
.getAllByRole('combobox')
221+
.find(e => e.id === 'create-offer-receiver')!;
222+
fireEvent.change(receiverInput, { target: { value: 'bob::preapproval' } });
223+
await vi.waitFor(() => expect(screen.getByRole('button', { name: 'Send' })).toBeEnabled());
224+
225+
// there should be no description input
226+
expect(screen.queryByRole('textbox', { name: 'description' })).not.toBeInTheDocument();
227+
228+
await user.click(screen.getByRole('button', { name: 'Send' }));
229+
230+
await assertCorrectMockIsCalled(
231+
true,
232+
{
233+
amount: '1.0',
234+
receiver_party_id: 'bob::preapproval',
235+
// description omitted: it is not sent
236+
},
237+
true
238+
);
239+
});
240+
241+
test('transfer offers have description field when unsupported for preapprovals', async () => {
242+
// transfer preapprovals do not support description, but that's inconsequential to regular transfer offers
243+
server.use(featureSupportHandler(false, false));
244+
const user = userEvent.setup();
245+
render(
246+
<WalletConfigProvider>
247+
<App />
248+
</WalletConfigProvider>
249+
);
250+
expect(await screen.findByText('Transfer')).toBeDefined();
251+
252+
const transferOffersLink = screen.getByRole('link', { name: 'Transfer' });
253+
await user.click(transferOffersLink);
254+
expect(screen.getByRole('heading', { name: 'Transfers' })).toBeDefined();
255+
256+
const receiverInput = screen
257+
.getAllByRole('combobox')
258+
.find(e => e.id === 'create-offer-receiver')!;
259+
fireEvent.change(receiverInput, { target: { value: 'bob::preapproval' } });
260+
await vi.waitFor(() => expect(screen.getByRole('button', { name: 'Send' })).toBeEnabled());
261+
262+
await user.click(screen.getByRole('checkbox'));
263+
264+
const description = 'Works';
265+
const descriptionInput = screen.getByRole('textbox', { name: 'description' });
266+
await user.type(descriptionInput, description);
267+
268+
await user.click(screen.getByRole('button', { name: 'Send' }));
269+
270+
await assertCorrectMockIsCalled(
271+
true,
272+
{
273+
amount: '1.0',
274+
receiver_party_id: 'bob::preapproval',
275+
description,
276+
},
277+
false
278+
);
279+
});
280+
281+
test('transfer offers have description field when party has no preapproval and transfer preapprovals do not support descriptions', async () => {
282+
// transfer preapprovals do not support description, but that's inconsequential to regular transfer offers
283+
server.use(featureSupportHandler(false, false));
284+
const user = userEvent.setup();
285+
render(
286+
<WalletConfigProvider>
287+
<App />
288+
</WalletConfigProvider>
289+
);
290+
expect(await screen.findByText('Transfer')).toBeDefined();
291+
292+
const transferOffersLink = screen.getByRole('link', { name: 'Transfer' });
293+
await user.click(transferOffersLink);
294+
expect(screen.getByRole('heading', { name: 'Transfers' })).toBeDefined();
295+
296+
const receiverInput = screen
297+
.getAllByRole('combobox')
298+
.find(e => e.id === 'create-offer-receiver')!;
299+
fireEvent.change(receiverInput, { target: { value: 'bob::nopreapproval' } });
300+
await vi.waitFor(() => expect(screen.getByRole('button', { name: 'Send' })).toBeEnabled());
301+
302+
const description = 'Works';
303+
const descriptionInput = screen.getByRole('textbox', { name: 'description' });
304+
await user.type(descriptionInput, description);
305+
306+
await user.click(screen.getByRole('button', { name: 'Send' }));
307+
308+
await assertCorrectMockIsCalled(
309+
true,
310+
{
311+
amount: '1.0',
312+
receiver_party_id: 'bob::nopreapproval',
313+
description,
314+
},
315+
false
316+
);
317+
});
195318
}, 7500);
196319

197320
function transferTests(disableTokenStandard: boolean) {
@@ -203,7 +326,7 @@ function transferTests(disableTokenStandard: boolean) {
203326
}
204327

205328
test('transfer offer is used when receiver has no transfer preapproval', async () => {
206-
server.use(featureSupportHandler(true));
329+
server.use(featureSupportHandler(true, true));
207330
const user = userEvent.setup();
208331
render(
209332
<WalletConfigProvider>
@@ -236,7 +359,7 @@ function transferTests(disableTokenStandard: boolean) {
236359
});
237360

238361
test('transfer preapproval is used when receiver has a transfer preapproval', async () => {
239-
server.use(featureSupportHandler(true));
362+
server.use(featureSupportHandler(true, true));
240363
const user = userEvent.setup();
241364
render(
242365
<WalletConfigProvider>
@@ -270,7 +393,7 @@ function transferTests(disableTokenStandard: boolean) {
270393
});
271394

272395
test('transfer offer is used when receiver has a transfer preapproval but checkbox is unchecked', async () => {
273-
server.use(featureSupportHandler(true));
396+
server.use(featureSupportHandler(true, true));
274397
const user = userEvent.setup();
275398
render(
276399
<WalletConfigProvider>
@@ -304,7 +427,7 @@ function transferTests(disableTokenStandard: boolean) {
304427
});
305428

306429
test('deduplication id is passed', async () => {
307-
server.use(featureSupportHandler(true));
430+
server.use(featureSupportHandler(true, true));
308431
const user = userEvent.setup();
309432
render(
310433
<WalletConfigProvider>
@@ -370,7 +493,7 @@ function transferTests(disableTokenStandard: boolean) {
370493

371494
async function assertCorrectMockIsCalled(
372495
usesRegularTransferOffer: boolean,
373-
expected: { amount: string; receiver_party_id: string; description: string },
496+
expected: { amount: string; receiver_party_id: string; description?: string },
374497
isPreapproval: boolean
375498
) {
376499
if (!usesRegularTransferOffer) {
@@ -383,6 +506,12 @@ async function assertCorrectMockIsCalled(
383506
expect(requestMocks.transferPreapprovalSend).toHaveBeenCalledWith(
384507
expect.objectContaining(expected)
385508
);
509+
// unfortunately 'objectContaining' does not work for `description: undefined`:
510+
// the mock omits the field and the expected has it as undefined.
511+
// Omitting `description` from `expected` doesn't work because then it's not checked at all.
512+
expect(requestMocks.transferPreapprovalSend.mock.lastCall![0].description).equals(
513+
expected.description
514+
);
386515
expect(requestMocks.createTransferOffer).not.toHaveBeenCalled();
387516
expect(requestMocks.createTransferViaTokenStandard).not.toHaveBeenCalled();
388517
} else {

apps/wallet/frontend/src/components/SendTransfer.tsx

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -252,18 +252,22 @@ const SendTransfer: React.FC = () => {
252252
</FormControl>
253253
</Stack>
254254
)}
255-
<Stack direction="column" mb={4} spacing={1}>
256-
<Typography variant="h6">
257-
Description <Typography variant="caption">(optional)</Typography>{' '}
258-
</Typography>
259-
<TextField
260-
id="create-offer-description"
261-
rows={4}
262-
multiline
263-
inputProps={{ 'aria-label': 'description' }}
264-
onChange={e => setDescription(e.target.value)}
265-
/>
266-
</Stack>
255+
{featureSupport.data?.transferPreapprovalDescription ||
256+
!useTransferPreapproval ||
257+
!preapprovalResult.data ? (
258+
<Stack direction="column" mb={4} spacing={1}>
259+
<Typography variant="h6">
260+
Description <Typography variant="caption">(optional)</Typography>{' '}
261+
</Typography>
262+
<TextField
263+
id="create-offer-description"
264+
rows={4}
265+
multiline
266+
inputProps={{ 'aria-label': 'description' }}
267+
onChange={e => setDescription(e.target.value)}
268+
/>
269+
</Stack>
270+
) : null}
267271

268272
<DisableConditionally
269273
conditions={[

apps/wallet/frontend/src/contexts/WalletServiceContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ export const WalletClientProvider: React.FC<React.PropsWithChildren<WalletProps>
276276
receiver_party_id: receiverPartyId,
277277
amount: amount.isInteger() ? amount.toFixed(1) : amount.toString(),
278278
deduplication_id: deduplicationId,
279-
description,
279+
description: description === '' ? undefined : description,
280280
};
281281
await walletClient.transferPreapprovalSend(request);
282282
},

0 commit comments

Comments
 (0)