@layerzerolabs/common-canton 1.2.68 → 1.2.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +140 -138
- package/package.json +10 -10
- package/src/refresh/README.md +119 -0
- package/src/refresh/address-resolver.ts +27 -19
- package/src/refresh/index.ts +2 -0
- package/src/refresh/plan.ts +176 -0
- package/src/refresh/resolve-plan.ts +502 -0
- package/src/refresh/resolve.ts +21 -2
- package/src/transaction.ts +3 -0
- package/test/refresh/resolve-plan.test.ts +723 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { NonRetryableError } from '@layerzerolabs/common-error-utils';
|
|
4
|
+
|
|
5
|
+
import type { ICantonClient } from '../../src/client/interface';
|
|
6
|
+
import { cantonActiveContractIdRef } from '../../src/deferredValue';
|
|
7
|
+
import type { JsActiveContract } from '../../src/open-api/models';
|
|
8
|
+
import { resolveCantonRefreshPlan, selectContractsByGte } from '../../src/refresh';
|
|
9
|
+
import { type CantonRefreshPlan, cantonRefreshPlanSchema } from '../../src/refresh/plan';
|
|
10
|
+
import { cantonTransactionSchema, type CantonUnresolvedCommands } from '../../src/transaction';
|
|
11
|
+
|
|
12
|
+
const RICH = 'rich::namespace';
|
|
13
|
+
const DSO = 'dso::namespace';
|
|
14
|
+
const INFORMEE = 'informee::namespace';
|
|
15
|
+
|
|
16
|
+
const activeContract = (args: {
|
|
17
|
+
contractId: string;
|
|
18
|
+
templateId: string;
|
|
19
|
+
createArgument: Record<string, unknown>;
|
|
20
|
+
signatories: string[];
|
|
21
|
+
observers?: string[];
|
|
22
|
+
blob?: string;
|
|
23
|
+
}): JsActiveContract =>
|
|
24
|
+
({
|
|
25
|
+
createdEvent: {
|
|
26
|
+
offset: 1,
|
|
27
|
+
nodeId: 0,
|
|
28
|
+
contractId: args.contractId,
|
|
29
|
+
templateId: args.templateId,
|
|
30
|
+
createArgument: args.createArgument,
|
|
31
|
+
signatories: args.signatories,
|
|
32
|
+
observers: args.observers ?? [],
|
|
33
|
+
createdEventBlob: args.blob ?? `blob-${args.contractId}`,
|
|
34
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
35
|
+
packageName: 'package',
|
|
36
|
+
representativePackageId: 'package-id',
|
|
37
|
+
acsDelta: true,
|
|
38
|
+
},
|
|
39
|
+
synchronizerId: 'sync',
|
|
40
|
+
reassignmentCounter: 0,
|
|
41
|
+
}) as JsActiveContract;
|
|
42
|
+
|
|
43
|
+
const amulet = (contractId: string, amount: string): JsActiveContract =>
|
|
44
|
+
activeContract({
|
|
45
|
+
contractId,
|
|
46
|
+
templateId: 'pkg:Splice.Amulet:Amulet',
|
|
47
|
+
createArgument: { owner: RICH, amount: { initialAmount: amount } },
|
|
48
|
+
signatories: [DSO],
|
|
49
|
+
observers: [RICH],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
type ExerciseCommand = Extract<CantonUnresolvedCommands[number], { exerciseCommand: unknown }>;
|
|
53
|
+
type CreateCommand = Extract<CantonUnresolvedCommands[number], { createCommand: unknown }>;
|
|
54
|
+
|
|
55
|
+
const delegateCommand = (): ExerciseCommand => ({
|
|
56
|
+
exerciseCommand: {
|
|
57
|
+
templateId: 'pkg:Executor:Executor',
|
|
58
|
+
contractId: 'stale-executor',
|
|
59
|
+
choice: 'DelegateLzReceive',
|
|
60
|
+
choiceArgument: {
|
|
61
|
+
amuletCids: ['stale-amulet'],
|
|
62
|
+
amuletContext: { values: { stale: true } },
|
|
63
|
+
timestamp: '2020-01-01T00:00:00.000Z',
|
|
64
|
+
callArgs: [['timestamp', { tag: 'AV_Time', value: '2020-01-01T00:00:00.000Z' }]],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const timedCreateCommand = (): CreateCommand => ({
|
|
70
|
+
createCommand: {
|
|
71
|
+
templateId: 'pkg:Timed:Timed',
|
|
72
|
+
createArguments: {
|
|
73
|
+
executeBefore: '2020-01-01T00:00:00.000Z',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const getExercise = (command: CantonUnresolvedCommands[number]) => {
|
|
79
|
+
if (!('exerciseCommand' in command)) throw new Error('Expected exercise command');
|
|
80
|
+
return command.exerciseCommand;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const plan = (): CantonRefreshPlan => ({
|
|
84
|
+
commands: [
|
|
85
|
+
{
|
|
86
|
+
resolver: {
|
|
87
|
+
type: 'activeContract',
|
|
88
|
+
selector: {
|
|
89
|
+
queryAs: { party: RICH },
|
|
90
|
+
templateId: '#layerzero-executor:Executor:Executor',
|
|
91
|
+
expectedSignatories: [RICH],
|
|
92
|
+
},
|
|
93
|
+
selection: { kind: 'exactlyOne' },
|
|
94
|
+
},
|
|
95
|
+
targets: [
|
|
96
|
+
{
|
|
97
|
+
commandIndex: 0,
|
|
98
|
+
command: 'exercise',
|
|
99
|
+
field: 'contractId',
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
resolver: {
|
|
105
|
+
type: 'activeContract',
|
|
106
|
+
selector: {
|
|
107
|
+
queryAs: { party: RICH },
|
|
108
|
+
templateId: '#splice-amulet:Splice.Amulet:Amulet',
|
|
109
|
+
expectedSignatories: [DSO],
|
|
110
|
+
stateFilter: { owner: RICH },
|
|
111
|
+
},
|
|
112
|
+
selection: { kind: 'gte', field: 'amount', value: '5.0' },
|
|
113
|
+
},
|
|
114
|
+
targets: [
|
|
115
|
+
{
|
|
116
|
+
commandIndex: 0,
|
|
117
|
+
command: 'exercise',
|
|
118
|
+
choice: 'DelegateLzReceive',
|
|
119
|
+
arguments: [{ path: [{ field: 'amuletCids' }] }],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
resolver: {
|
|
125
|
+
type: 'amuletContext',
|
|
126
|
+
dsoParty: DSO,
|
|
127
|
+
},
|
|
128
|
+
targets: [
|
|
129
|
+
{
|
|
130
|
+
commandIndex: 0,
|
|
131
|
+
command: 'exercise',
|
|
132
|
+
choice: 'DelegateLzReceive',
|
|
133
|
+
arguments: [{ path: [{ field: 'amuletContext' }] }],
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
resolver: {
|
|
139
|
+
type: 'timestamp',
|
|
140
|
+
},
|
|
141
|
+
targets: [
|
|
142
|
+
{
|
|
143
|
+
commandIndex: 0,
|
|
144
|
+
command: 'exercise',
|
|
145
|
+
choice: 'DelegateLzReceive',
|
|
146
|
+
arguments: [
|
|
147
|
+
{ path: [{ field: 'timestamp' }] },
|
|
148
|
+
{
|
|
149
|
+
path: [{ field: 'callArgs' }, { damlMapKey: 'timestamp' }],
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
disclosures: {
|
|
157
|
+
excludeBaselineCids: ['stale-rules'],
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('Canton refresh plan', () => {
|
|
162
|
+
it('round-trips through the populated transaction schema', () => {
|
|
163
|
+
const refreshPlan = plan();
|
|
164
|
+
const parsed = cantonTransactionSchema.parse({
|
|
165
|
+
chainName: 'canton',
|
|
166
|
+
timestamps: { created: 1 },
|
|
167
|
+
populatedTransaction: {
|
|
168
|
+
unresolvedCommands: [delegateCommand()],
|
|
169
|
+
unresolvedDisclosedContracts: [],
|
|
170
|
+
actAs: [RICH],
|
|
171
|
+
refreshPlan,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
expect(JSON.parse(JSON.stringify(parsed.populatedTransaction.refreshPlan))).toEqual(
|
|
175
|
+
refreshPlan,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('requires choice for choice-argument targets', () => {
|
|
180
|
+
expect(
|
|
181
|
+
cantonRefreshPlanSchema.safeParse({
|
|
182
|
+
commands: [
|
|
183
|
+
{
|
|
184
|
+
resolver: { type: 'timestamp' },
|
|
185
|
+
targets: [
|
|
186
|
+
{
|
|
187
|
+
commandIndex: 0,
|
|
188
|
+
command: 'exercise',
|
|
189
|
+
arguments: [{ path: [{ field: 'timestamp' }] }],
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
}).success,
|
|
195
|
+
).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('restricts command resolvers to compatible targets', () => {
|
|
199
|
+
const contractIdTarget = {
|
|
200
|
+
commandIndex: 0,
|
|
201
|
+
command: 'exercise',
|
|
202
|
+
field: 'contractId',
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
expect(
|
|
206
|
+
cantonRefreshPlanSchema.safeParse({
|
|
207
|
+
commands: [
|
|
208
|
+
{
|
|
209
|
+
resolver: { type: 'timestamp' },
|
|
210
|
+
targets: [contractIdTarget],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
}).success,
|
|
214
|
+
).toBe(false);
|
|
215
|
+
expect(
|
|
216
|
+
cantonRefreshPlanSchema.safeParse({
|
|
217
|
+
commands: [
|
|
218
|
+
{
|
|
219
|
+
resolver: { type: 'amuletContext', dsoParty: DSO },
|
|
220
|
+
targets: [contractIdTarget],
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
}).success,
|
|
224
|
+
).toBe(false);
|
|
225
|
+
expect(
|
|
226
|
+
cantonRefreshPlanSchema.safeParse({
|
|
227
|
+
commands: [
|
|
228
|
+
{
|
|
229
|
+
resolver: {
|
|
230
|
+
type: 'activeContract',
|
|
231
|
+
selector: {
|
|
232
|
+
queryAs: { party: RICH },
|
|
233
|
+
templateId: '#splice-amulet:Splice.Amulet:Amulet',
|
|
234
|
+
expectedSignatories: [DSO],
|
|
235
|
+
},
|
|
236
|
+
selection: { kind: 'gte', field: 'amount', value: '5.0' },
|
|
237
|
+
},
|
|
238
|
+
targets: [contractIdTarget],
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
}).success,
|
|
242
|
+
).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('requires timestamp targets to have a nested path', () => {
|
|
246
|
+
expect(
|
|
247
|
+
cantonRefreshPlanSchema.safeParse({
|
|
248
|
+
commands: [
|
|
249
|
+
{
|
|
250
|
+
resolver: { type: 'timestamp' },
|
|
251
|
+
targets: [
|
|
252
|
+
{
|
|
253
|
+
commandIndex: 0,
|
|
254
|
+
command: 'exercise',
|
|
255
|
+
choice: 'DelegateLzReceive',
|
|
256
|
+
arguments: [{ path: [] }],
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
}).success,
|
|
262
|
+
).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('requires baseline disclosure exclusions to be non-empty and unique', () => {
|
|
266
|
+
const refreshPlan = plan();
|
|
267
|
+
expect(
|
|
268
|
+
cantonRefreshPlanSchema.safeParse({
|
|
269
|
+
...refreshPlan,
|
|
270
|
+
disclosures: { excludeBaselineCids: [] },
|
|
271
|
+
}).success,
|
|
272
|
+
).toBe(false);
|
|
273
|
+
expect(
|
|
274
|
+
cantonRefreshPlanSchema.safeParse({
|
|
275
|
+
...refreshPlan,
|
|
276
|
+
disclosures: { excludeBaselineCids: ['stale-rules', 'stale-rules'] },
|
|
277
|
+
}).success,
|
|
278
|
+
).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('rejects excluding a disclosure that is not in the persisted baseline', async () => {
|
|
282
|
+
await expect(
|
|
283
|
+
resolveCantonRefreshPlan({
|
|
284
|
+
unresolvedCommands: [delegateCommand()],
|
|
285
|
+
unresolvedDisclosedContracts: [],
|
|
286
|
+
refreshPlan: plan(),
|
|
287
|
+
signingParties: [RICH],
|
|
288
|
+
client: {} as ICantonClient,
|
|
289
|
+
}),
|
|
290
|
+
).rejects.toThrow(/not present in unresolvedDisclosedContracts/);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('applies timestamps after external refreshes regardless of declaration order', async () => {
|
|
294
|
+
vi.useFakeTimers();
|
|
295
|
+
try {
|
|
296
|
+
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
|
297
|
+
const client = {
|
|
298
|
+
getAmuletContextWithExternalRules: vi.fn(async () => {
|
|
299
|
+
vi.setSystemTime(new Date('2026-01-01T00:01:00.000Z'));
|
|
300
|
+
return {
|
|
301
|
+
amuletContext: { values: { fresh: true } },
|
|
302
|
+
externalPartyAmuletRulesCid: 'rules',
|
|
303
|
+
disclosedContracts: [],
|
|
304
|
+
};
|
|
305
|
+
}),
|
|
306
|
+
} as unknown as ICantonClient;
|
|
307
|
+
|
|
308
|
+
const result = await resolveCantonRefreshPlan({
|
|
309
|
+
unresolvedCommands: [delegateCommand()],
|
|
310
|
+
unresolvedDisclosedContracts: [],
|
|
311
|
+
refreshPlan: {
|
|
312
|
+
commands: [
|
|
313
|
+
{
|
|
314
|
+
resolver: { type: 'timestamp' },
|
|
315
|
+
targets: [
|
|
316
|
+
{
|
|
317
|
+
commandIndex: 0,
|
|
318
|
+
command: 'exercise',
|
|
319
|
+
choice: 'DelegateLzReceive',
|
|
320
|
+
arguments: [{ path: [{ field: 'timestamp' }] }],
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
resolver: { type: 'amuletContext', dsoParty: DSO },
|
|
326
|
+
targets: [
|
|
327
|
+
{
|
|
328
|
+
commandIndex: 0,
|
|
329
|
+
command: 'exercise',
|
|
330
|
+
choice: 'DelegateLzReceive',
|
|
331
|
+
arguments: [{ path: [{ field: 'amuletContext' }] }],
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
signingParties: [RICH],
|
|
338
|
+
client,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const exercise = getExercise(result.commands[0]!);
|
|
342
|
+
expect(exercise.choiceArgument).toMatchObject({
|
|
343
|
+
timestamp: '2026-01-01T00:01:00.000Z',
|
|
344
|
+
amuletContext: { values: { fresh: true } },
|
|
345
|
+
});
|
|
346
|
+
} finally {
|
|
347
|
+
vi.useRealTimers();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('rejects timestamp targets whose current value is not a Canton Time', async () => {
|
|
352
|
+
const command = delegateCommand();
|
|
353
|
+
(command.exerciseCommand.choiceArgument as Record<string, unknown>).guid = 'not-a-time';
|
|
354
|
+
|
|
355
|
+
await expect(
|
|
356
|
+
resolveCantonRefreshPlan({
|
|
357
|
+
unresolvedCommands: [command],
|
|
358
|
+
unresolvedDisclosedContracts: [],
|
|
359
|
+
refreshPlan: {
|
|
360
|
+
commands: [
|
|
361
|
+
{
|
|
362
|
+
resolver: { type: 'timestamp' },
|
|
363
|
+
targets: [
|
|
364
|
+
{
|
|
365
|
+
commandIndex: 0,
|
|
366
|
+
command: 'exercise',
|
|
367
|
+
choice: 'DelegateLzReceive',
|
|
368
|
+
arguments: [{ path: [{ field: 'guid' }] }],
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
signingParties: [RICH],
|
|
375
|
+
client: {} as ICantonClient,
|
|
376
|
+
}),
|
|
377
|
+
).rejects.toThrow(/must contain a Canton Time/);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('patches a timestamp inside nested DAML maps', async () => {
|
|
381
|
+
vi.useFakeTimers();
|
|
382
|
+
try {
|
|
383
|
+
vi.setSystemTime(new Date('2026-02-03T04:05:06.000Z'));
|
|
384
|
+
const command = delegateCommand();
|
|
385
|
+
(command.exerciseCommand.choiceArgument as Record<string, unknown>).nestedMaps = [
|
|
386
|
+
['outer', [['inner', { tag: 'AV_Time', value: '2020-01-01T00:00:00.000Z' }]]],
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
const result = await resolveCantonRefreshPlan({
|
|
390
|
+
unresolvedCommands: [command],
|
|
391
|
+
unresolvedDisclosedContracts: [],
|
|
392
|
+
refreshPlan: {
|
|
393
|
+
commands: [
|
|
394
|
+
{
|
|
395
|
+
resolver: { type: 'timestamp' },
|
|
396
|
+
targets: [
|
|
397
|
+
{
|
|
398
|
+
commandIndex: 0,
|
|
399
|
+
command: 'exercise',
|
|
400
|
+
choice: 'DelegateLzReceive',
|
|
401
|
+
arguments: [
|
|
402
|
+
{
|
|
403
|
+
path: [
|
|
404
|
+
{ field: 'nestedMaps' },
|
|
405
|
+
{ damlMapKey: 'outer' },
|
|
406
|
+
{ damlMapKey: 'inner' },
|
|
407
|
+
],
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
},
|
|
415
|
+
signingParties: [RICH],
|
|
416
|
+
client: {} as ICantonClient,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
expect(getExercise(result.commands[0]!).choiceArgument).toMatchObject({
|
|
420
|
+
nestedMaps: [
|
|
421
|
+
['outer', [['inner', { tag: 'AV_Time', value: '2026-02-03T04:05:06.000Z' }]]],
|
|
422
|
+
],
|
|
423
|
+
});
|
|
424
|
+
} finally {
|
|
425
|
+
vi.useRealTimers();
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('patches guarded create arguments with a per-patch offset', async () => {
|
|
430
|
+
vi.useFakeTimers();
|
|
431
|
+
try {
|
|
432
|
+
vi.setSystemTime(new Date('2026-02-03T04:05:06.000Z'));
|
|
433
|
+
const result = await resolveCantonRefreshPlan({
|
|
434
|
+
unresolvedCommands: [timedCreateCommand()],
|
|
435
|
+
unresolvedDisclosedContracts: [],
|
|
436
|
+
refreshPlan: {
|
|
437
|
+
commands: [
|
|
438
|
+
{
|
|
439
|
+
resolver: { type: 'timestamp' },
|
|
440
|
+
targets: [
|
|
441
|
+
{
|
|
442
|
+
commandIndex: 0,
|
|
443
|
+
command: 'create',
|
|
444
|
+
template: 'pkg:Timed:Timed',
|
|
445
|
+
arguments: [
|
|
446
|
+
{
|
|
447
|
+
path: [{ field: 'executeBefore' }],
|
|
448
|
+
offsetMs: 60_000,
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
signingParties: [RICH],
|
|
457
|
+
client: {} as ICantonClient,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
expect(result.commands[0]).toMatchObject({
|
|
461
|
+
createCommand: {
|
|
462
|
+
createArguments: {
|
|
463
|
+
executeBefore: '2026-02-03T04:06:06.000Z',
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
} finally {
|
|
468
|
+
vi.useRealTimers();
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('uses latest ACS reads and one instant while replacing commands and disclosures', async () => {
|
|
473
|
+
const executor = activeContract({
|
|
474
|
+
contractId: 'fresh-executor',
|
|
475
|
+
templateId: 'pkg:Executor:Executor',
|
|
476
|
+
createArgument: {},
|
|
477
|
+
signatories: [RICH],
|
|
478
|
+
});
|
|
479
|
+
const queryActiveContracts = vi.fn(
|
|
480
|
+
async (_party: string, offset: number, opts?: { templateIds?: string[] }) => {
|
|
481
|
+
expect(offset).toBe(42);
|
|
482
|
+
if (opts?.templateIds?.[0]?.includes('Executor')) return [executor];
|
|
483
|
+
return [amulet('amulet-3', '3.0'), amulet('amulet-4', '4.0')];
|
|
484
|
+
},
|
|
485
|
+
);
|
|
486
|
+
const client = {
|
|
487
|
+
getLedgerEndOffset: vi.fn(async () => 42),
|
|
488
|
+
queryActiveContracts,
|
|
489
|
+
getAmuletContextWithExternalRules: vi.fn(async () => ({
|
|
490
|
+
amuletContext: { values: { fresh: true } },
|
|
491
|
+
externalPartyAmuletRulesCid: 'fresh-rules',
|
|
492
|
+
disclosedContracts: [
|
|
493
|
+
{
|
|
494
|
+
contractId: 'fresh-rules',
|
|
495
|
+
templateId: 'pkg:ExternalPartyAmuletRules',
|
|
496
|
+
createdEventBlob: 'fresh-rules-blob',
|
|
497
|
+
synchronizerId: 'sync',
|
|
498
|
+
},
|
|
499
|
+
],
|
|
500
|
+
})),
|
|
501
|
+
} as unknown as ICantonClient;
|
|
502
|
+
const unresolvedCommands = [delegateCommand()];
|
|
503
|
+
|
|
504
|
+
const result = await resolveCantonRefreshPlan({
|
|
505
|
+
unresolvedCommands,
|
|
506
|
+
unresolvedDisclosedContracts: [
|
|
507
|
+
{
|
|
508
|
+
contractId: 'static',
|
|
509
|
+
templateId: 'pkg:Static',
|
|
510
|
+
createdEventBlob: 'static-blob',
|
|
511
|
+
synchronizerId: 'sync',
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
contractId: 'stale-rules',
|
|
515
|
+
templateId: 'pkg:ExternalPartyAmuletRules',
|
|
516
|
+
createdEventBlob: 'stale-rules-blob',
|
|
517
|
+
synchronizerId: 'sync',
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
refreshPlan: plan(),
|
|
521
|
+
signingParties: [RICH],
|
|
522
|
+
client,
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const exercise = getExercise(result.commands[0]!);
|
|
526
|
+
expect(exercise.contractId).toBe('fresh-executor');
|
|
527
|
+
expect(exercise.choiceArgument).toMatchObject({
|
|
528
|
+
amuletCids: ['amulet-4', 'amulet-3'],
|
|
529
|
+
amuletContext: { values: { fresh: true } },
|
|
530
|
+
});
|
|
531
|
+
const args = exercise.choiceArgument as {
|
|
532
|
+
timestamp: string;
|
|
533
|
+
callArgs: Array<[string, { tag: string; value: string }]>;
|
|
534
|
+
};
|
|
535
|
+
expect(args.callArgs[0]![1]).toEqual({
|
|
536
|
+
tag: 'AV_Time',
|
|
537
|
+
value: args.timestamp,
|
|
538
|
+
});
|
|
539
|
+
expect(result.disclosedContracts.map(({ contractId }) => contractId)).toEqual([
|
|
540
|
+
'static',
|
|
541
|
+
'fresh-rules',
|
|
542
|
+
]);
|
|
543
|
+
expect(client.getLedgerEndOffset).toHaveBeenCalledTimes(2);
|
|
544
|
+
expect(queryActiveContracts).toHaveBeenCalledTimes(2);
|
|
545
|
+
expect(queryActiveContracts).toHaveBeenCalledWith(
|
|
546
|
+
RICH,
|
|
547
|
+
42,
|
|
548
|
+
expect.objectContaining({ includeCreatedEventBlob: true }),
|
|
549
|
+
);
|
|
550
|
+
expect(getExercise(unresolvedCommands[0]!).contractId).toBe('stale-executor');
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('replaces a stale disclosure with the coherent ACS disclosure', async () => {
|
|
554
|
+
const fresh = activeContract({
|
|
555
|
+
contractId: 'fresh-config',
|
|
556
|
+
templateId: 'pkg:Config:Config',
|
|
557
|
+
createArgument: { owner: RICH },
|
|
558
|
+
signatories: [RICH],
|
|
559
|
+
observers: [INFORMEE],
|
|
560
|
+
blob: 'fresh-blob',
|
|
561
|
+
});
|
|
562
|
+
const client = {
|
|
563
|
+
getLedgerEndOffset: vi.fn(async () => 9),
|
|
564
|
+
queryActiveContracts: vi.fn(async () => [fresh]),
|
|
565
|
+
} as unknown as ICantonClient;
|
|
566
|
+
const command = delegateCommand();
|
|
567
|
+
(command.exerciseCommand.choiceArgument as Record<string, unknown>).configCid =
|
|
568
|
+
'stale-config';
|
|
569
|
+
|
|
570
|
+
const result = await resolveCantonRefreshPlan({
|
|
571
|
+
unresolvedCommands: [command],
|
|
572
|
+
unresolvedDisclosedContracts: [
|
|
573
|
+
{
|
|
574
|
+
contractId: 'stale-config',
|
|
575
|
+
templateId: 'pkg:Config:Config',
|
|
576
|
+
createdEventBlob: 'stale-blob',
|
|
577
|
+
synchronizerId: 'sync',
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
refreshPlan: {
|
|
581
|
+
commands: [
|
|
582
|
+
{
|
|
583
|
+
resolver: {
|
|
584
|
+
type: 'activeContract',
|
|
585
|
+
selector: {
|
|
586
|
+
queryAs: { party: INFORMEE },
|
|
587
|
+
templateId: '#package:Config:Config',
|
|
588
|
+
expectedSignatories: [RICH],
|
|
589
|
+
},
|
|
590
|
+
selection: { kind: 'exactlyOne' },
|
|
591
|
+
},
|
|
592
|
+
targets: [
|
|
593
|
+
{
|
|
594
|
+
commandIndex: 0,
|
|
595
|
+
command: 'exercise',
|
|
596
|
+
choice: 'DelegateLzReceive',
|
|
597
|
+
arguments: [{ path: [{ field: 'configCid' }] }],
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
disclosures: {
|
|
603
|
+
excludeBaselineCids: ['stale-config'],
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
signingParties: ['different-signer'],
|
|
607
|
+
client,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
expect(result.disclosedContracts).toEqual([
|
|
611
|
+
{
|
|
612
|
+
contractId: 'fresh-config',
|
|
613
|
+
templateId: 'pkg:Config:Config',
|
|
614
|
+
createdEventBlob: 'fresh-blob',
|
|
615
|
+
synchronizerId: 'sync',
|
|
616
|
+
},
|
|
617
|
+
]);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('does not mutate unresolved commands when a later target is invalid', async () => {
|
|
621
|
+
const command = delegateCommand();
|
|
622
|
+
const client = {
|
|
623
|
+
getLedgerEndOffset: vi.fn(async () => 1),
|
|
624
|
+
getAmuletContextWithExternalRules: vi.fn(async () => ({
|
|
625
|
+
amuletContext: { values: { fresh: true } },
|
|
626
|
+
externalPartyAmuletRulesCid: 'rules',
|
|
627
|
+
disclosedContracts: [],
|
|
628
|
+
})),
|
|
629
|
+
} as unknown as ICantonClient;
|
|
630
|
+
|
|
631
|
+
await expect(
|
|
632
|
+
resolveCantonRefreshPlan({
|
|
633
|
+
unresolvedCommands: [command],
|
|
634
|
+
unresolvedDisclosedContracts: [],
|
|
635
|
+
refreshPlan: {
|
|
636
|
+
commands: [
|
|
637
|
+
{
|
|
638
|
+
resolver: { type: 'timestamp' },
|
|
639
|
+
targets: [
|
|
640
|
+
{
|
|
641
|
+
commandIndex: 0,
|
|
642
|
+
command: 'exercise',
|
|
643
|
+
choice: 'DelegateLzReceive',
|
|
644
|
+
arguments: [{ path: [{ field: 'timestamp' }] }],
|
|
645
|
+
},
|
|
646
|
+
],
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
resolver: { type: 'amuletContext', dsoParty: DSO },
|
|
650
|
+
targets: [
|
|
651
|
+
{
|
|
652
|
+
commandIndex: 0,
|
|
653
|
+
command: 'exercise',
|
|
654
|
+
choice: 'WrongChoice',
|
|
655
|
+
arguments: [{ path: [{ field: 'amuletContext' }] }],
|
|
656
|
+
},
|
|
657
|
+
],
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
},
|
|
661
|
+
signingParties: [RICH],
|
|
662
|
+
client,
|
|
663
|
+
}),
|
|
664
|
+
).rejects.toBeInstanceOf(NonRetryableError);
|
|
665
|
+
expect(command.exerciseCommand.choiceArgument).toMatchObject({
|
|
666
|
+
timestamp: '2020-01-01T00:00:00.000Z',
|
|
667
|
+
amuletContext: { values: { stale: true } },
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('rejects mixing a refresh plan with legacy sentinels', async () => {
|
|
672
|
+
const command = delegateCommand();
|
|
673
|
+
command.exerciseCommand.contractId = cantonActiveContractIdRef({
|
|
674
|
+
partyToFilter: { party: RICH },
|
|
675
|
+
expectedSignatories: [RICH],
|
|
676
|
+
templateId: '#package:Executor:Executor',
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
await expect(
|
|
680
|
+
resolveCantonRefreshPlan({
|
|
681
|
+
unresolvedCommands: [command],
|
|
682
|
+
unresolvedDisclosedContracts: [],
|
|
683
|
+
refreshPlan: plan(),
|
|
684
|
+
signingParties: [RICH],
|
|
685
|
+
client: {
|
|
686
|
+
getLedgerEndOffset: vi.fn(async () => 1),
|
|
687
|
+
} as unknown as ICantonClient,
|
|
688
|
+
}),
|
|
689
|
+
).rejects.toThrow(/cannot be combined/);
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
describe('selectContractsByGte', () => {
|
|
694
|
+
it('selects deterministically until the requested field value is covered', () => {
|
|
695
|
+
expect(
|
|
696
|
+
selectContractsByGte(
|
|
697
|
+
[amulet('cid-b', '2.0'), amulet('cid-a', '2.0'), amulet('cid-c', '4.0')],
|
|
698
|
+
'amount',
|
|
699
|
+
'5.0',
|
|
700
|
+
).map(({ createdEvent }) => createdEvent.contractId),
|
|
701
|
+
).toEqual(['cid-c', 'cid-a']);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('fails when the combined balance is insufficient', () => {
|
|
705
|
+
expect(() => selectContractsByGte([amulet('cid-a', '1.0')], 'amount', '2.0')).toThrow(
|
|
706
|
+
/Insufficient contract value/,
|
|
707
|
+
);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('supports a different numeric create-argument field', () => {
|
|
711
|
+
const credit = activeContract({
|
|
712
|
+
contractId: 'credit-cid',
|
|
713
|
+
templateId: 'pkg:Asset:Credit',
|
|
714
|
+
createArgument: { availableCredit: '3.0' },
|
|
715
|
+
signatories: [RICH],
|
|
716
|
+
});
|
|
717
|
+
expect(
|
|
718
|
+
selectContractsByGte([credit], 'availableCredit', '2.0').map(
|
|
719
|
+
({ createdEvent }) => createdEvent.contractId,
|
|
720
|
+
),
|
|
721
|
+
).toEqual(['credit-cid']);
|
|
722
|
+
});
|
|
723
|
+
});
|