@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.
@@ -1,5 +1,7 @@
1
1
  export * from './address-resolver';
2
2
  export * from './deployment-address';
3
+ export * from './plan';
3
4
  export * from './resolve';
5
+ export * from './resolve-plan';
4
6
  export * from './reverse-substitute';
5
7
  export * from './wrapped-lz-sdk';
@@ -0,0 +1,176 @@
1
+ import { z } from 'zod';
2
+
3
+ import { cantonExpectedSignatoriesSchema, cantonRefreshPartyFilterSchema } from '../deferredValue';
4
+
5
+ export const cantonRefreshPathSegmentSchema = z.union([
6
+ z.object({ field: z.string().min(1) }),
7
+ z.object({ index: z.number().int().nonnegative() }),
8
+ z.object({ damlMapKey: z.string().min(1) }),
9
+ ]);
10
+
11
+ export const cantonRefreshPathSchema = z.array(cantonRefreshPathSegmentSchema);
12
+ const cantonNestedRefreshPathSchema = cantonRefreshPathSchema.min(1);
13
+
14
+ const exerciseEntrypointTargetSchema = z.object({
15
+ commandIndex: z.number().int().nonnegative(),
16
+ command: z.literal('exercise'),
17
+ field: z.literal('contractId'),
18
+ });
19
+
20
+ const exerciseChoiceArgumentTargetSchema = z.object({
21
+ commandIndex: z.number().int().nonnegative(),
22
+ command: z.literal('exercise'),
23
+ choice: z.string().min(1),
24
+ arguments: z
25
+ .array(
26
+ z.object({
27
+ path: cantonRefreshPathSchema,
28
+ }),
29
+ )
30
+ .min(1),
31
+ });
32
+
33
+ const cantonCreateArgumentTargetSchema = z.object({
34
+ commandIndex: z.number().int().nonnegative(),
35
+ command: z.literal('create'),
36
+ template: z.string().min(1),
37
+ arguments: z
38
+ .array(
39
+ z.object({
40
+ path: cantonRefreshPathSchema,
41
+ }),
42
+ )
43
+ .min(1),
44
+ });
45
+
46
+ export const cantonRefreshTargetSchema = z.union([
47
+ exerciseEntrypointTargetSchema,
48
+ exerciseChoiceArgumentTargetSchema,
49
+ cantonCreateArgumentTargetSchema,
50
+ ]);
51
+
52
+ export const cantonArgumentRefreshTargetSchema = z.union([
53
+ exerciseChoiceArgumentTargetSchema,
54
+ cantonCreateArgumentTargetSchema,
55
+ ]);
56
+
57
+ const cantonNestedArgumentRefreshTargetSchema = z.union([
58
+ exerciseChoiceArgumentTargetSchema.extend({
59
+ arguments: z
60
+ .array(
61
+ z.object({
62
+ path: cantonNestedRefreshPathSchema,
63
+ offsetMs: z.number().int().optional(),
64
+ }),
65
+ )
66
+ .min(1),
67
+ }),
68
+ cantonCreateArgumentTargetSchema.extend({
69
+ arguments: z
70
+ .array(
71
+ z.object({
72
+ path: cantonNestedRefreshPathSchema,
73
+ offsetMs: z.number().int().optional(),
74
+ }),
75
+ )
76
+ .min(1),
77
+ }),
78
+ ]);
79
+
80
+ const cantonContractSelectorSchema = z.object({
81
+ queryAs: cantonRefreshPartyFilterSchema,
82
+ templateId: z.string().min(1),
83
+ expectedSignatories: cantonExpectedSignatoriesSchema,
84
+ stateFilter: z.record(z.string(), z.unknown()).optional(),
85
+ });
86
+
87
+ const cantonExactlyOneSelectionSchema = z.object({ kind: z.literal('exactlyOne') });
88
+ const cantonAllSelectionSchema = z.object({ kind: z.literal('all') });
89
+ const cantonGteSelectionSchema = z.object({
90
+ kind: z.literal('gte'),
91
+ field: z.string().min(1),
92
+ value: z.string().min(1),
93
+ });
94
+
95
+ export const cantonActiveContractSelectionSchema = z.discriminatedUnion('kind', [
96
+ cantonExactlyOneSelectionSchema,
97
+ cantonAllSelectionSchema,
98
+ cantonGteSelectionSchema,
99
+ ]);
100
+
101
+ const cantonActiveContractResolverFields = {
102
+ type: z.literal('activeContract'),
103
+ selector: cantonContractSelectorSchema,
104
+ };
105
+
106
+ const cantonActiveContractRefreshSchema = z.union([
107
+ z.object({
108
+ resolver: z.object({
109
+ ...cantonActiveContractResolverFields,
110
+ selection: cantonExactlyOneSelectionSchema,
111
+ }),
112
+ targets: z.array(cantonRefreshTargetSchema).min(1),
113
+ }),
114
+ z.object({
115
+ resolver: z.object({
116
+ ...cantonActiveContractResolverFields,
117
+ selection: z.union([cantonAllSelectionSchema, cantonGteSelectionSchema]),
118
+ }),
119
+ targets: z.array(cantonArgumentRefreshTargetSchema).min(1),
120
+ }),
121
+ ]);
122
+
123
+ const cantonAmuletContextRefreshSchema = z.object({
124
+ resolver: z.object({
125
+ type: z.literal('amuletContext'),
126
+ dsoParty: z.string().min(1),
127
+ }),
128
+ targets: z.array(cantonArgumentRefreshTargetSchema).min(1),
129
+ });
130
+
131
+ const cantonTimestampRefreshSchema = z.object({
132
+ resolver: z.object({
133
+ type: z.literal('timestamp'),
134
+ }),
135
+ targets: z.array(cantonNestedArgumentRefreshTargetSchema).min(1),
136
+ });
137
+
138
+ export const cantonRefreshSchema = z.union([
139
+ cantonActiveContractRefreshSchema,
140
+ cantonAmuletContextRefreshSchema,
141
+ cantonTimestampRefreshSchema,
142
+ ]);
143
+
144
+ const cantonDisclosureRefreshSchema = z.object({
145
+ excludeBaselineCids: z
146
+ .array(z.string().min(1))
147
+ .min(1)
148
+ .refine((cids) => new Set(cids).size === cids.length, {
149
+ message: 'Baseline disclosure CIDs must be unique',
150
+ }),
151
+ });
152
+
153
+ export const cantonRefreshPlanSchema = z.object({
154
+ commands: z.array(cantonRefreshSchema).min(1),
155
+ disclosures: cantonDisclosureRefreshSchema.optional(),
156
+ });
157
+
158
+ export type CantonRefreshPathSegment = z.infer<typeof cantonRefreshPathSegmentSchema>;
159
+ export type CantonRefreshPath = z.infer<typeof cantonRefreshPathSchema>;
160
+ export type ExerciseEntrypointTargetField = Pick<
161
+ z.infer<typeof exerciseEntrypointTargetSchema>,
162
+ 'field'
163
+ >;
164
+ export type ExerciseChoiceArgumentTargetField = Pick<
165
+ z.infer<typeof exerciseChoiceArgumentTargetSchema>,
166
+ 'choice' | 'arguments'
167
+ >;
168
+ export type CantonRefreshTarget = z.infer<typeof cantonRefreshTargetSchema>;
169
+ export type CantonArgumentRefreshTarget = z.infer<typeof cantonArgumentRefreshTargetSchema>;
170
+ export type CantonContractSelector = z.infer<typeof cantonContractSelectorSchema>;
171
+ export type CantonActiveContractSelection = z.infer<typeof cantonActiveContractSelectionSchema>;
172
+ export type CantonActiveContractRefresh = z.infer<typeof cantonActiveContractRefreshSchema>;
173
+ export type CantonAmuletContextRefresh = z.infer<typeof cantonAmuletContextRefreshSchema>;
174
+ export type CantonTimestampRefresh = z.infer<typeof cantonTimestampRefreshSchema>;
175
+ export type CantonRefresh = z.infer<typeof cantonRefreshSchema>;
176
+ export type CantonRefreshPlan = z.infer<typeof cantonRefreshPlanSchema>;
@@ -0,0 +1,502 @@
1
+ import { NonRetryableError } from '@layerzerolabs/common-error-utils';
2
+
3
+ import type { ICantonClient } from '../client/interface';
4
+ import {
5
+ type CantonRefreshPartyFilter,
6
+ isCantonDeferredSender,
7
+ isCantonDeferredSentinel,
8
+ } from '../deferredValue';
9
+ import type { JsActiveContract } from '../open-api/models';
10
+ import type {
11
+ CantonCommands,
12
+ CantonDisclosedContracts,
13
+ CantonUnresolvedCommands,
14
+ } from '../transaction';
15
+ import { getCreateArgumentField } from '../trusted-origin';
16
+ import { type ActiveContractRef, createContractAddressResolver } from './address-resolver';
17
+ import type {
18
+ CantonActiveContractRefresh,
19
+ CantonActiveContractSelection,
20
+ CantonAmuletContextRefresh,
21
+ CantonArgumentRefreshTarget,
22
+ CantonContractSelector,
23
+ CantonRefresh,
24
+ CantonRefreshPath,
25
+ CantonRefreshPlan,
26
+ CantonRefreshTarget,
27
+ CantonTimestampRefresh,
28
+ } from './plan';
29
+
30
+ const DAML_DECIMAL_SCALE = 10n ** 10n;
31
+ const MAX_SELECTED_CONTRACTS = 100;
32
+ const CANTON_TIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/;
33
+
34
+ const nonRetryable = (message: string): never => {
35
+ throw new NonRetryableError(message);
36
+ };
37
+
38
+ const resolveParty = (
39
+ partyRef: CantonRefreshPartyFilter,
40
+ senderParty: string | undefined,
41
+ label: string,
42
+ ): string => {
43
+ if (!isCantonDeferredSender(partyRef)) return partyRef.party;
44
+ if (!senderParty) {
45
+ return nonRetryable(`${label} requires a resolved sender party`);
46
+ }
47
+ return senderParty;
48
+ };
49
+
50
+ const parseDamlDecimal = (value: unknown, label: string): bigint => {
51
+ if (typeof value !== 'string') {
52
+ return nonRetryable(`${label} must be a DAML Decimal string`);
53
+ }
54
+ const match = /^([+-]?)(\d+)(?:\.(\d{0,10}))?$/.exec(value);
55
+ if (!match) {
56
+ return nonRetryable(`${label} is not a valid DAML Decimal: ${value}`);
57
+ }
58
+ const sign = match[1] === '-' ? -1n : 1n;
59
+ const whole = BigInt(match[2]!);
60
+ const fraction = BigInt((match[3] ?? '').padEnd(10, '0'));
61
+ return sign * (whole * DAML_DECIMAL_SCALE + fraction);
62
+ };
63
+
64
+ const getContractNumericField = (contract: JsActiveContract, field: string): bigint => {
65
+ const raw = getCreateArgumentField(contract.createdEvent.createArgument, field);
66
+ const value =
67
+ raw !== null && typeof raw === 'object' && 'initialAmount' in raw
68
+ ? (raw as { initialAmount?: unknown }).initialAmount
69
+ : raw;
70
+ return parseDamlDecimal(
71
+ value,
72
+ `Contract ${contract.createdEvent.contractId} numeric field ${field}`,
73
+ );
74
+ };
75
+
76
+ export const selectContractsByGte = (
77
+ contracts: JsActiveContract[],
78
+ field: string,
79
+ requestedValue: string,
80
+ ): JsActiveContract[] => {
81
+ const requested = parseDamlDecimal(requestedValue, 'Requested contract value');
82
+ if (requested <= 0n) {
83
+ return nonRetryable('Requested contract value must be positive');
84
+ }
85
+
86
+ const candidates = contracts
87
+ .map((contract) => ({
88
+ contract,
89
+ amount: getContractNumericField(contract, field),
90
+ }))
91
+ .filter(({ amount }) => amount > 0n)
92
+ .sort((left, right) =>
93
+ left.amount === right.amount
94
+ ? left.contract.createdEvent.contractId.localeCompare(
95
+ right.contract.createdEvent.contractId,
96
+ )
97
+ : left.amount > right.amount
98
+ ? -1
99
+ : 1,
100
+ );
101
+
102
+ const selected: JsActiveContract[] = [];
103
+ let total = 0n;
104
+ for (const candidate of candidates) {
105
+ selected.push(candidate.contract);
106
+ total += candidate.amount;
107
+ if (total >= requested) break;
108
+ }
109
+
110
+ if (total < requested) {
111
+ return nonRetryable(
112
+ `Insufficient contract value in field ${field}: requested ${requestedValue}, found ${total.toString()} base units`,
113
+ );
114
+ }
115
+ if (selected.length > MAX_SELECTED_CONTRACTS) {
116
+ return nonRetryable(
117
+ `Selection needs ${selected.length} contracts, exceeding the ${MAX_SELECTED_CONTRACTS} input limit`,
118
+ );
119
+ }
120
+ return selected;
121
+ };
122
+
123
+ const makeActiveContractRef = (selector: CantonContractSelector): ActiveContractRef => ({
124
+ type: 'activeContractIds',
125
+ partyToFilter: selector.queryAs,
126
+ expectedSignatories: selector.expectedSignatories,
127
+ templateId: selector.templateId,
128
+ contractStateFilter: selector.stateFilter,
129
+ });
130
+
131
+ const selectActiveContracts = (
132
+ contracts: JsActiveContract[],
133
+ selection: CantonActiveContractSelection,
134
+ ): JsActiveContract[] => {
135
+ switch (selection.kind) {
136
+ case 'exactlyOne':
137
+ if (contracts.length !== 1) {
138
+ return nonRetryable(
139
+ `Active contract selection matched ${contracts.length} contracts, expected exactly one`,
140
+ );
141
+ }
142
+ return contracts;
143
+ case 'all':
144
+ return contracts;
145
+ case 'gte':
146
+ return selectContractsByGte(contracts, selection.field, selection.value);
147
+ }
148
+ };
149
+
150
+ export const resolveCantonActiveContracts = async (args: {
151
+ client: ICantonClient;
152
+ selector: CantonContractSelector;
153
+ selection: CantonActiveContractSelection;
154
+ senderParty?: string;
155
+ }): Promise<{ contractIds: string[] }> => {
156
+ const queryParty = resolveParty(
157
+ args.selector.queryAs,
158
+ args.senderParty,
159
+ 'Active contract selection',
160
+ );
161
+ const ref = makeActiveContractRef(args.selector);
162
+ const resolver = createContractAddressResolver({
163
+ client: args.client,
164
+ senderParty: args.senderParty,
165
+ signingParties: [queryParty],
166
+ appendDisclosures: () => undefined,
167
+ });
168
+ const selected = selectActiveContracts(
169
+ await resolver.resolveActiveContracts(ref),
170
+ args.selection,
171
+ );
172
+ return {
173
+ contractIds: selected.map(({ createdEvent }) => createdEvent.contractId),
174
+ };
175
+ };
176
+
177
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
178
+ value !== null && typeof value === 'object' && !Array.isArray(value);
179
+
180
+ const isCantonTime = (value: unknown): value is string =>
181
+ typeof value === 'string' &&
182
+ CANTON_TIME_PATTERN.test(value) &&
183
+ Number.isFinite(Date.parse(value));
184
+
185
+ const isActiveContractRefresh = (refresh: CantonRefresh): refresh is CantonActiveContractRefresh =>
186
+ refresh.resolver.type === 'activeContract';
187
+
188
+ const isAmuletContextRefresh = (refresh: CantonRefresh): refresh is CantonAmuletContextRefresh =>
189
+ refresh.resolver.type === 'amuletContext';
190
+
191
+ const isTimestampRefresh = (refresh: CantonRefresh): refresh is CantonTimestampRefresh =>
192
+ refresh.resolver.type === 'timestamp';
193
+
194
+ const replaceAtPath = (root: unknown, path: CantonRefreshPath, value: unknown): unknown => {
195
+ if (path.length === 0) return value;
196
+
197
+ let current = root;
198
+ for (const [position, segment] of path.entries()) {
199
+ const isLast = position === path.length - 1;
200
+ if ('field' in segment) {
201
+ if (!isRecord(current) || !(segment.field in current)) {
202
+ return nonRetryable(`Refresh target field "${segment.field}" does not exist`);
203
+ }
204
+ if (isLast) {
205
+ current[segment.field] = value;
206
+ return root;
207
+ }
208
+ current = current[segment.field];
209
+ continue;
210
+ }
211
+ if ('index' in segment) {
212
+ if (!Array.isArray(current) || segment.index >= current.length) {
213
+ return nonRetryable(`Refresh target array index ${segment.index} does not exist`);
214
+ }
215
+ if (isLast) {
216
+ current[segment.index] = value;
217
+ return root;
218
+ }
219
+ current = current[segment.index];
220
+ continue;
221
+ }
222
+
223
+ if (!Array.isArray(current)) {
224
+ return nonRetryable(`Refresh target DAML map "${segment.damlMapKey}" is not an array`);
225
+ }
226
+ const matches = current.filter(
227
+ (entry): entry is [string, unknown] =>
228
+ Array.isArray(entry) && entry.length === 2 && entry[0] === segment.damlMapKey,
229
+ );
230
+ if (matches.length !== 1) {
231
+ return nonRetryable(
232
+ `Refresh target DAML map key "${segment.damlMapKey}" matched ${matches.length} entries`,
233
+ );
234
+ }
235
+ if (isLast) {
236
+ matches[0]![1] = value;
237
+ return root;
238
+ }
239
+ current = matches[0]![1];
240
+ }
241
+ return root;
242
+ };
243
+
244
+ const readAtPath = (root: unknown, path: CantonRefreshPath): unknown => {
245
+ let current = root;
246
+ for (const segment of path) {
247
+ if ('field' in segment) {
248
+ if (!isRecord(current) || !(segment.field in current)) {
249
+ return nonRetryable(`Refresh target field "${segment.field}" does not exist`);
250
+ }
251
+ current = current[segment.field];
252
+ continue;
253
+ }
254
+ if ('index' in segment) {
255
+ if (!Array.isArray(current) || segment.index >= current.length) {
256
+ return nonRetryable(`Refresh target array index ${segment.index} does not exist`);
257
+ }
258
+ current = current[segment.index];
259
+ continue;
260
+ }
261
+
262
+ if (!Array.isArray(current)) {
263
+ return nonRetryable(`Refresh target DAML map "${segment.damlMapKey}" is not an array`);
264
+ }
265
+ const matches = current.filter(
266
+ (entry): entry is [string, unknown] =>
267
+ Array.isArray(entry) && entry.length === 2 && entry[0] === segment.damlMapKey,
268
+ );
269
+ if (matches.length !== 1) {
270
+ return nonRetryable(
271
+ `Refresh target DAML map key "${segment.damlMapKey}" matched ${matches.length} entries`,
272
+ );
273
+ }
274
+ current = matches[0]![1];
275
+ }
276
+ return current;
277
+ };
278
+
279
+ const readTarget = (
280
+ commands: CantonUnresolvedCommands,
281
+ target: CantonArgumentRefreshTarget,
282
+ path: CantonRefreshPath,
283
+ ): unknown => {
284
+ const command = commands[target.commandIndex];
285
+ if (!command) {
286
+ return nonRetryable(`Refresh command index ${target.commandIndex} does not exist`);
287
+ }
288
+
289
+ if (target.command === 'exercise') {
290
+ if (!('exerciseCommand' in command)) {
291
+ return nonRetryable(`Refresh command index ${target.commandIndex} is not an exercise`);
292
+ }
293
+ if (command.exerciseCommand.choice !== target.choice) {
294
+ return nonRetryable(
295
+ `Refresh command index ${target.commandIndex} expected choice ${target.choice}, found ${command.exerciseCommand.choice}`,
296
+ );
297
+ }
298
+ return readAtPath(command.exerciseCommand.choiceArgument, path);
299
+ }
300
+
301
+ if (!('createCommand' in command)) {
302
+ return nonRetryable(`Refresh command index ${target.commandIndex} is not a create`);
303
+ }
304
+ if (command.createCommand.templateId !== target.template) {
305
+ return nonRetryable(
306
+ `Refresh command index ${target.commandIndex} expected template ${target.template}, found ${command.createCommand.templateId}`,
307
+ );
308
+ }
309
+ return readAtPath(command.createCommand.createArguments, path);
310
+ };
311
+
312
+ const applyPatch = (
313
+ commands: CantonUnresolvedCommands,
314
+ target: CantonArgumentRefreshTarget,
315
+ path: CantonRefreshPath,
316
+ value: unknown,
317
+ ): void => {
318
+ const command = commands[target.commandIndex];
319
+ if (!command) {
320
+ return nonRetryable(`Refresh command index ${target.commandIndex} does not exist`);
321
+ }
322
+
323
+ if (target.command === 'exercise') {
324
+ if (!('exerciseCommand' in command)) {
325
+ return nonRetryable(`Refresh command index ${target.commandIndex} is not an exercise`);
326
+ }
327
+ if (command.exerciseCommand.choice !== target.choice) {
328
+ return nonRetryable(
329
+ `Refresh command index ${target.commandIndex} expected choice ${target.choice}, found ${command.exerciseCommand.choice}`,
330
+ );
331
+ }
332
+ command.exerciseCommand.choiceArgument = replaceAtPath(
333
+ command.exerciseCommand.choiceArgument,
334
+ path,
335
+ value,
336
+ );
337
+ return;
338
+ }
339
+
340
+ if (!('createCommand' in command)) {
341
+ return nonRetryable(`Refresh command index ${target.commandIndex} is not a create`);
342
+ }
343
+ if (command.createCommand.templateId !== target.template) {
344
+ return nonRetryable(
345
+ `Refresh command index ${target.commandIndex} expected template ${target.template}, found ${command.createCommand.templateId}`,
346
+ );
347
+ }
348
+ command.createCommand.createArguments = replaceAtPath(
349
+ command.createCommand.createArguments,
350
+ path,
351
+ value,
352
+ );
353
+ };
354
+
355
+ const applyTarget = (
356
+ commands: CantonUnresolvedCommands,
357
+ target: CantonRefreshTarget,
358
+ value: unknown,
359
+ ): void => {
360
+ if ('field' in target) {
361
+ const command = commands[target.commandIndex];
362
+ if (!command) {
363
+ return nonRetryable(`Refresh command index ${target.commandIndex} does not exist`);
364
+ }
365
+ if (!('exerciseCommand' in command)) {
366
+ return nonRetryable(`Refresh command index ${target.commandIndex} is not an exercise`);
367
+ }
368
+ if (typeof value !== 'string') {
369
+ return nonRetryable('Exercise contract ID refresh must resolve to a string');
370
+ }
371
+ command.exerciseCommand.contractId = value;
372
+ return;
373
+ }
374
+
375
+ for (const argument of target.arguments) {
376
+ applyPatch(commands, target, argument.path, value);
377
+ }
378
+ };
379
+
380
+ const containsDeferredSentinel = (value: unknown, key?: string): boolean => {
381
+ if (key === 'createdEventBlob') return false;
382
+ if (isCantonDeferredSentinel(value)) return true;
383
+ if (Array.isArray(value)) return value.some((item) => containsDeferredSentinel(item));
384
+ if (!isRecord(value)) return false;
385
+ return Object.entries(value).some(([childKey, child]) =>
386
+ containsDeferredSentinel(child, childKey),
387
+ );
388
+ };
389
+
390
+ const disclosuresEqual = (
391
+ left: CantonDisclosedContracts[number],
392
+ right: CantonDisclosedContracts[number],
393
+ ): boolean =>
394
+ left.contractId === right.contractId &&
395
+ left.templateId === right.templateId &&
396
+ left.createdEventBlob === right.createdEventBlob &&
397
+ left.synchronizerId === right.synchronizerId;
398
+
399
+ export const resolveCantonRefreshPlan = async (args: {
400
+ unresolvedCommands: CantonUnresolvedCommands;
401
+ unresolvedDisclosedContracts: CantonDisclosedContracts;
402
+ refreshPlan: CantonRefreshPlan;
403
+ senderParty?: string;
404
+ signingParties: string[];
405
+ client: ICantonClient;
406
+ }): Promise<{ commands: CantonCommands; disclosedContracts: CantonDisclosedContracts }> => {
407
+ if (
408
+ containsDeferredSentinel(args.unresolvedCommands) ||
409
+ containsDeferredSentinel(args.unresolvedDisclosedContracts)
410
+ ) {
411
+ return nonRetryable('Canton refreshPlan cannot be combined with deferred sentinels');
412
+ }
413
+
414
+ const commands = structuredClone(args.unresolvedCommands);
415
+ const baselineDisclosureCids = new Set(
416
+ args.unresolvedDisclosedContracts.map(({ contractId }) => contractId),
417
+ );
418
+ const excludedBaselineCids = new Set(args.refreshPlan.disclosures?.excludeBaselineCids ?? []);
419
+ for (const contractId of excludedBaselineCids) {
420
+ if (!baselineDisclosureCids.has(contractId)) {
421
+ return nonRetryable(
422
+ `Cannot exclude baseline disclosure ${contractId}: it is not present in unresolvedDisclosedContracts`,
423
+ );
424
+ }
425
+ }
426
+ const disclosureMap = new Map<string, CantonDisclosedContracts[number]>();
427
+ const appendDisclosures = (disclosures: CantonDisclosedContracts): void => {
428
+ for (const disclosure of disclosures) {
429
+ const existing = disclosureMap.get(disclosure.contractId);
430
+ if (existing && !disclosuresEqual(existing, disclosure)) {
431
+ return nonRetryable(
432
+ `Conflicting disclosures for contract ${disclosure.contractId}`,
433
+ );
434
+ }
435
+ disclosureMap.set(disclosure.contractId, disclosure);
436
+ }
437
+ };
438
+ appendDisclosures(
439
+ args.unresolvedDisclosedContracts.filter(
440
+ ({ contractId }) => !excludedBaselineCids.has(contractId),
441
+ ),
442
+ );
443
+
444
+ const resolver = createContractAddressResolver({
445
+ client: args.client,
446
+ senderParty: args.senderParty,
447
+ signingParties: args.signingParties,
448
+ appendDisclosures,
449
+ });
450
+
451
+ for (const refresh of args.refreshPlan.commands) {
452
+ if (isActiveContractRefresh(refresh)) {
453
+ const ref = makeActiveContractRef(refresh.resolver.selector);
454
+ const selected = selectActiveContracts(
455
+ await resolver.resolveActiveContracts(ref),
456
+ refresh.resolver.selection,
457
+ );
458
+ await resolver.discloseActiveContracts(ref, selected);
459
+ const contractIds = selected.map(({ createdEvent }) => createdEvent.contractId);
460
+ const resolved =
461
+ refresh.resolver.selection.kind === 'exactlyOne' ? contractIds[0]! : contractIds;
462
+ for (const target of refresh.targets) applyTarget(commands, target, resolved);
463
+ continue;
464
+ }
465
+
466
+ if (isAmuletContextRefresh(refresh)) {
467
+ const bundle = await args.client.getAmuletContextWithExternalRules({
468
+ admin: refresh.resolver.dsoParty,
469
+ id: 'Amulet',
470
+ });
471
+ appendDisclosures(bundle.disclosedContracts);
472
+ for (const target of refresh.targets) {
473
+ applyTarget(commands, target, bundle.amuletContext);
474
+ }
475
+ }
476
+ }
477
+
478
+ // Timestamp refreshes run after all external reads so their shared instant is
479
+ // as close as possible to prepareForSigning, regardless of declaration order.
480
+ const now = new Date();
481
+ for (const refresh of args.refreshPlan.commands) {
482
+ if (!isTimestampRefresh(refresh)) continue;
483
+
484
+ for (const target of refresh.targets) {
485
+ for (const { path, offsetMs = 0 } of target.arguments) {
486
+ const iso = new Date(now.getTime() + offsetMs).toISOString();
487
+ const current = readTarget(commands, target, path);
488
+ const value = isCantonTime(current)
489
+ ? iso
490
+ : isRecord(current) && current.tag === 'AV_Time' && isCantonTime(current.value)
491
+ ? { ...current, value: iso }
492
+ : nonRetryable('Timestamp refresh target must contain a Canton Time value');
493
+ applyPatch(commands, target, path, value);
494
+ }
495
+ }
496
+ }
497
+
498
+ return {
499
+ commands: commands as CantonCommands,
500
+ disclosedContracts: [...disclosureMap.values()],
501
+ };
502
+ };