@crediolabs/policy-synth 0.1.8 → 0.1.9

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.
@@ -307,6 +307,10 @@ function lowerSelector(s) {
307
307
  switch (s.kind) {
308
308
  case 'arg':
309
309
  return { kind: 'call_arg', index: s.argIndex };
310
+ case 'arg_len':
311
+ return { kind: 'call_arg_len', index: s.argIndex };
312
+ case 'arg_field':
313
+ return { kind: 'call_arg_field', index: s.argIndex, element: s.element, field: s.field };
310
314
  case 'amount':
311
315
  return { kind: 'amount', token: s.token };
312
316
  case 'window_spent':
@@ -329,13 +333,15 @@ function lowerSelector(s) {
329
333
  * selector kind to the matching `literal_*` kind. The IR compare value is a
330
334
  * raw string (i128-safe); the selector kind fixes the canonical wire type:
331
335
  * - arg -> IR scalarType (set by the recorder/parser)
336
+ * - arg_len -> u32 (vec length is a small non-negative integer)
337
+ * - arg_field -> IR scalarType (the field's recorded type)
332
338
  * - amount -> i128 (canonical Stellar token amount encoding)
333
339
  * - window_spent -> i128 (canonical amount encoding)
334
340
  * - oracle_price -> i128 (canonical price encoding)
335
341
  * - invocation_count -> u32 (counts are small non-negative integers)
336
342
  * - now / valid_until -> u64 (unix timestamps in seconds) */
337
343
  function literalFromIRCompare(c) {
338
- const scalarType = c.selector.kind === 'arg' ? c.selector.scalarType : literalScalarForSelector(c.selector.kind);
344
+ const scalarType = selectorScalarType(c.selector);
339
345
  return literalFromScalar(c.value, scalarType);
340
346
  }
341
347
  function literalScalarForSelector(kind) {
@@ -345,14 +351,16 @@ function literalScalarForSelector(kind) {
345
351
  case 'oracle_price':
346
352
  return 'i128';
347
353
  case 'invocation_count':
354
+ case 'arg_len':
348
355
  return 'u32';
349
356
  case 'now':
350
357
  case 'valid_until':
351
358
  return 'u64';
352
359
  case 'arg':
360
+ case 'arg_field':
353
361
  case 'calldata':
354
362
  case 'value':
355
- // arg -> caller handles scalarType; calldata/value -> unreachable (Path-B).
363
+ // arg / arg_field -> caller handles scalarType; calldata/value -> unreachable (Path-B).
356
364
  return 'i128';
357
365
  }
358
366
  }
@@ -380,10 +388,12 @@ function literalFromScalar(value, scalarType) {
380
388
  /** Scalar type of an IRSelector for the purpose of building literal leaves
381
389
  * (the right-hand side of `eq` / elements of an `in` haystack / elements of
382
390
  * a `literal_vec`). Mirrors `literalScalarForSelector` for OZ extensions and
383
- * uses the selector's own `scalarType` for `arg` selectors. */
391
+ * uses the selector's own `scalarType` for `arg` and `arg_field` selectors. */
384
392
  function selectorScalarType(selector) {
385
393
  if (selector.kind === 'arg')
386
394
  return selector.scalarType;
395
+ if (selector.kind === 'arg_field')
396
+ return selector.scalarType;
387
397
  return literalScalarForSelector(selector.kind);
388
398
  }
389
399
  /** Walk a condition tree and throw if any oracle_price leaf is found anywhere
@@ -457,6 +467,10 @@ function describeCondition(cond) {
457
467
  return `per-call amount comparison on ${s.token}`;
458
468
  case 'arg':
459
469
  return `argument comparison on arg ${s.argIndex}`;
470
+ case 'arg_len':
471
+ return `vec length comparison on arg ${s.argIndex}`;
472
+ case 'arg_field':
473
+ return `map field comparison on arg ${s.argIndex}.${s.field}`;
460
474
  case 'calldata':
461
475
  return 'EVM calldata comparison';
462
476
  case 'value':
@@ -472,6 +486,10 @@ function describeSelector(s) {
472
486
  switch (s.kind) {
473
487
  case 'arg':
474
488
  return `arg ${s.argIndex}`;
489
+ case 'arg_len':
490
+ return `arg_len(${s.argIndex})`;
491
+ case 'arg_field':
492
+ return `arg_field(${s.argIndex}, ${s.element}, ${s.field})`;
475
493
  case 'amount':
476
494
  return `amount(${s.token})`;
477
495
  case 'window_spent':
@@ -229,6 +229,10 @@ function describeCondition(cond) {
229
229
  return `per-call amount comparison on ${s.token} (predicate DSL)`;
230
230
  case 'arg':
231
231
  return `argument comparison on arg ${s.argIndex} (predicate DSL)`;
232
+ case 'arg_len':
233
+ return `vec length comparison on arg ${s.argIndex} (predicate DSL)`;
234
+ case 'arg_field':
235
+ return `map field comparison on arg ${s.argIndex}.${s.field} (predicate DSL)`;
232
236
  case 'calldata':
233
237
  return 'EVM calldata comparison (predicate DSL)';
234
238
  case 'value':
@@ -244,6 +248,10 @@ function describeSelector(s) {
244
248
  switch (s.kind) {
245
249
  case 'arg':
246
250
  return `arg ${s.argIndex}`;
251
+ case 'arg_len':
252
+ return `arg_len(${s.argIndex})`;
253
+ case 'arg_field':
254
+ return `arg_field(${s.argIndex}, ${s.element}, ${s.field})`;
247
255
  case 'amount':
248
256
  return `amount(${s.token})`;
249
257
  case 'window_spent':
@@ -14,6 +14,15 @@ export type IRSelector = {
14
14
  fieldIndex?: number;
15
15
  vecMode?: IRVecMode;
16
16
  scalarType: IRScalarType;
17
+ } | {
18
+ kind: 'arg_len';
19
+ argIndex: number;
20
+ } | {
21
+ kind: 'arg_field';
22
+ argIndex: number;
23
+ element: number;
24
+ field: string;
25
+ scalarType: IRScalarType;
17
26
  } | {
18
27
  kind: 'calldata';
19
28
  offset: number;
@@ -167,6 +167,15 @@ function encodeLeaf(leaf) {
167
167
  return xdr.ScVal.scvVec([symbol('call_fn')]);
168
168
  case 'call_arg':
169
169
  return xdr.ScVal.scvVec([symbol('call_arg'), xdr.ScVal.scvU32(leaf.index)]);
170
+ case 'call_arg_len':
171
+ return xdr.ScVal.scvVec([symbol('call_arg_len'), xdr.ScVal.scvU32(leaf.index)]);
172
+ case 'call_arg_field':
173
+ return xdr.ScVal.scvVec([
174
+ symbol('call_arg_field'),
175
+ xdr.ScVal.scvU32(leaf.index),
176
+ xdr.ScVal.scvU32(leaf.element),
177
+ xdr.ScVal.scvSymbol(leaf.field),
178
+ ]);
170
179
  case 'amount':
171
180
  return xdr.ScVal.scvVec([symbol('amount'), scvAddressFromStrkey(leaf.token)]);
172
181
  case 'window_spent':
@@ -184,6 +184,8 @@ function renderVecElement(leaf) {
184
184
  case 'call_contract':
185
185
  case 'call_fn':
186
186
  case 'call_arg':
187
+ case 'call_arg_len':
188
+ case 'call_arg_field':
187
189
  case 'amount':
188
190
  case 'window_spent':
189
191
  case 'now':
@@ -105,6 +105,8 @@ function renderVecElement(leaf) {
105
105
  case 'call_contract':
106
106
  case 'call_fn':
107
107
  case 'call_arg':
108
+ case 'call_arg_len':
109
+ case 'call_arg_field':
108
110
  case 'amount':
109
111
  case 'window_spent':
110
112
  case 'now':
@@ -276,6 +276,95 @@ function appendProtocolSpecificConstraints(ozConstraints, interpreterConstraints
276
276
  }
277
277
  }
278
278
  }
279
+ // Blend `submit` ONLY (not `claim`, whose vec arg is a vec<u32> of
280
+ // reserve_token_ids - no map fields to bind). The `requests` vec
281
+ // (call_arg[3]) is a vec<Request{ address, amount, request_type }>. Each
282
+ // Request is the per-reserve action selector - 0 Supply, 1 Withdraw,
283
+ // 2 SupplyCollateral, 3 WithdrawCollateral, 4 Borrow, 5 Repay, 6-9
284
+ // liquidation/auction fills. Pinning only one element is unsafe: a caller
285
+ // can append a second element with a different action (WithdrawCollateral
286
+ // -> Borrow on a different asset, any amount, then auction fills). Length
287
+ // + per-element pinning is total; a quantifier over elements is not. If we
288
+ // cannot emit BOTH, we surface AMBIGUITY rather than emit a partial bind.
289
+ if (protocol.protocol === 'blend' && protocol.fn === 'submit' && interpreterEnabled) {
290
+ const requestsArg = topLevel.args[3];
291
+ if (requestsArg && requestsArg.type === 'vec') {
292
+ const elements = requestsArg.value;
293
+ interpreterConstraints.push({
294
+ op: 'compare',
295
+ compare: {
296
+ selector: { kind: 'arg_len', argIndex: 3 },
297
+ operator: 'eq',
298
+ value: String(elements.length),
299
+ },
300
+ });
301
+ for (let i = 0; i < elements.length; i++) {
302
+ const element = elements[i];
303
+ if (!element || element.type !== 'map')
304
+ continue;
305
+ const fields = element.value;
306
+ const fieldValue = (name, scalarType) => {
307
+ const entry = fields.find((e) => e.key === name);
308
+ if (!entry)
309
+ return null;
310
+ if (entry.val.type === 'address' && scalarType === 'address')
311
+ return entry.val.value;
312
+ if (entry.val.type === 'i128' && scalarType === 'i128')
313
+ return entry.val.value;
314
+ if (entry.val.type === 'u32' && scalarType === 'u32')
315
+ return entry.val.value;
316
+ return null;
317
+ };
318
+ const address = fieldValue('address', 'address');
319
+ const amount = fieldValue('amount', 'i128');
320
+ const requestType = fieldValue('request_type', 'u32');
321
+ if (address === null || amount === null || requestType === null)
322
+ continue;
323
+ interpreterConstraints.push({
324
+ op: 'compare',
325
+ compare: {
326
+ selector: {
327
+ kind: 'arg_field',
328
+ argIndex: 3,
329
+ element: i,
330
+ field: 'request_type',
331
+ scalarType: 'u32',
332
+ },
333
+ operator: 'eq',
334
+ value: requestType,
335
+ },
336
+ });
337
+ interpreterConstraints.push({
338
+ op: 'compare',
339
+ compare: {
340
+ selector: {
341
+ kind: 'arg_field',
342
+ argIndex: 3,
343
+ element: i,
344
+ field: 'address',
345
+ scalarType: 'address',
346
+ },
347
+ operator: 'eq',
348
+ value: address,
349
+ },
350
+ });
351
+ interpreterConstraints.push({
352
+ op: 'compare',
353
+ compare: {
354
+ selector: {
355
+ kind: 'arg_field',
356
+ argIndex: 3,
357
+ element: i,
358
+ field: 'amount',
359
+ scalarType: 'i128',
360
+ },
361
+ operator: 'lte',
362
+ value: amount,
363
+ },
364
+ });
365
+ }
366
+ }
367
+ }
279
368
  // SoroSwap: the recorded hop path -> eq_seq on the path arg (call_arg[2]).
280
369
  // The interpreter adapter is the ONLY way to express an exact ordered
281
370
  // sequence (OZ built-ins cannot). Element order is preserved verbatim.
@@ -16,6 +16,15 @@ const ADJACENT_ASSETS = [
16
16
  // them as FINDINGS against the already-emitted policy.
17
17
  const OVERPERMISSIVE_DIMENSIONS = ['argument_reorder'];
18
18
  // The 15 dimensions the synth pipeline uses for self-verify and minimise.
19
+ // Phase 1 grammar extension: `vec_append` and `map_field_flip` are listed below
20
+ // alongside the existing dimensions so the per-element binds emitted for
21
+ // Blend `submit` (call_arg_len + 3 call_arg_field per element) survive
22
+ // minimise. Without these, no deny case exercises the new leaves and the
23
+ // minimise pass drops them as "redundant" - reopening the element-append
24
+ // hole they were added to close. Both dimensions are no-ops for any fixture
25
+ // whose predicate has no `call_arg_len` / `call_arg_field` leaves (the
26
+ // generator short-circuits on leaf-kind), so they cannot make production
27
+ // synthesis refuse for fixtures that do not exercise the new grammar.
19
28
  const ORIGINAL_DIMENSIONS = [
20
29
  'amount',
21
30
  'asset',
@@ -32,6 +41,8 @@ const ORIGINAL_DIMENSIONS = [
32
41
  'oracle_deviation_exceeded',
33
42
  'oracle_paused',
34
43
  'soroswap_allowed_path',
44
+ 'vec_append',
45
+ 'map_field_flip',
35
46
  ];
36
47
  export { ORIGINAL_DIMENSIONS, OVERPERMISSIVE_DIMENSIONS };
37
48
  /** Build deterministic model-evaluated alternatives without mutating the intended call.
@@ -191,7 +202,9 @@ export function generateCases(predicate, permitCtx, dimensions) {
191
202
  hasConstrainedArg &&
192
203
  permitCtx.args.length >= 2 &&
193
204
  permitCtx.args[0]?.type === 'address' &&
194
- permitCtx.args[1]?.type === 'address') {
205
+ permitCtx.args[1]?.type === 'address' &&
206
+ permitCtx.args[0].value !==
207
+ permitCtx.args[1].value) {
195
208
  const ctx = cloneContext(permitCtx);
196
209
  // Guard above guarantees args[0] and args[1] exist and are address-typed.
197
210
  const a0 = ctx.args[0];
@@ -200,6 +213,59 @@ export function generateCases(predicate, permitCtx, dimensions) {
200
213
  ctx.args[1] = a0;
201
214
  denies.push({ dimension: 'argument_reorder', ctx });
202
215
  }
216
+ // --- map_field_flip: flip a bound map field to a different valid value of
217
+ // the same type. Mirrors argument_reorder: OPT-IN only, never ORIGINAL.
218
+ // Targets each `call_arg_field` leaf and produces a ctx whose vec element
219
+ // has a different value of the recorded field (different request_type,
220
+ // different amount, different address). The blend-submit case uses this
221
+ // to detect a missing request_type pin. ---
222
+ if (!dimensions || dimensions.includes('map_field_flip')) {
223
+ for (const comparison of facts.comparisons) {
224
+ const sel = comparison.left;
225
+ if (sel.kind !== 'call_arg_field')
226
+ continue;
227
+ const arg = permitCtx.args[sel.index];
228
+ if (!arg || arg.type !== 'vec')
229
+ continue;
230
+ const element = arg.value[sel.element];
231
+ if (!element || element.type !== 'map' || !Array.isArray(element.value))
232
+ continue;
233
+ const entry = element.value.find((e) => e.key === sel.field);
234
+ if (!entry)
235
+ continue;
236
+ const flipped = flipFieldValue(entry.val);
237
+ if (flipped === null)
238
+ continue;
239
+ const ctx = cloneContext(permitCtx);
240
+ const clonedVec = arg.value.map(cloneScVal);
241
+ const clonedElement = clonedVec[sel.element];
242
+ if (clonedElement?.type !== 'map' || !Array.isArray(clonedElement.value))
243
+ continue;
244
+ clonedElement.value = clonedElement.value.map((e) => e.key === sel.field ? { key: e.key, val: flipped } : e);
245
+ clonedVec[sel.element] = clonedElement;
246
+ ctx.args[sel.index] = { type: 'vec', value: clonedVec };
247
+ denies.push({ dimension: 'map_field_flip', ctx });
248
+ }
249
+ }
250
+ // --- vec_append: append a new element to a bound vec. Mirrors map_field_flip:
251
+ // OPT-IN only, never ORIGINAL. Targets every `call_arg_len` leaf; without the
252
+ // length pin a caller can append an extra element to defeat per-element binds. ---
253
+ if (!dimensions || dimensions.includes('vec_append')) {
254
+ for (const comparison of facts.comparisons) {
255
+ const sel = comparison.left;
256
+ if (sel.kind !== 'call_arg_len')
257
+ continue;
258
+ const arg = permitCtx.args[sel.index];
259
+ if (!arg || arg.type !== 'vec')
260
+ continue;
261
+ const ctx = cloneContext(permitCtx);
262
+ ctx.args[sel.index] = {
263
+ type: 'vec',
264
+ value: [...arg.value, { type: 'other', value: 'deny-case-vec-append' }],
265
+ };
266
+ denies.push({ dimension: 'vec_append', ctx });
267
+ }
268
+ }
203
269
  // Version mismatch, malformed predicates, master authorization, and nonce replay are install-time checks and are intentionally omitted from model-evaluated cases.
204
270
  return { permit: cloneContext(permitCtx), denies };
205
271
  }
@@ -431,3 +497,25 @@ function cloneDepthError(value) {
431
497
  err.depthContext = value.type;
432
498
  throw err;
433
499
  }
500
+ /** Build a value of the SAME ScVal type that differs from the recorded one,
501
+ * used by the `map_field_flip` mutation to defeat a per-element pin without
502
+ * changing the wire type (a type/arity change is rejected by host dispatch
503
+ * before Policy::enforce, so it is not the predicate's job). Returns null
504
+ * when the ScVal type has no obvious different value (e.g. opaque/other). */
505
+ function flipFieldValue(val) {
506
+ switch (val.type) {
507
+ case 'address': {
508
+ const a = 'GBFKRGJYZXLTDEI36ZCQEIM225NMOCR2VDBOIHJTXJ54FEFFVL2FKALE';
509
+ const b = 'GD6XSMQJ47EHHJOWXQOND5YDVZC37JWZJHYHBKE6QJFSLLJ5KQXM5QS5';
510
+ return { type: 'address', value: val.value === a ? b : a };
511
+ }
512
+ case 'i128':
513
+ case 'u64':
514
+ case 'u32':
515
+ return { type: val.type, value: String(BigInt(val.value) + 1n) };
516
+ case 'symbol':
517
+ return { type: 'symbol', value: `${val.value}x` };
518
+ default:
519
+ return null;
520
+ }
521
+ }
@@ -175,6 +175,38 @@ function evalCompare(op, left, right, ctx) {
175
175
  return evalArgOrderedCompare(op, actual, right);
176
176
  return evalArgEq(op, actual, right, ctx);
177
177
  }
178
+ // --- step 4c: call_arg_len: the length of a vec-typed argument as a u32.
179
+ // Fails closed on a non-vec arg, an absent arg, or a non-u32 literal.
180
+ if (left.kind === 'call_arg_len') {
181
+ const actual = ctx.args[left.index];
182
+ if (!actual || actual.type !== 'vec')
183
+ return { permit: false, reason: 'ARG_MISMATCH' };
184
+ if (right.kind !== 'literal_u32')
185
+ return { permit: false, reason: 'ARG_MISMATCH' };
186
+ return actual.value.length === right.value
187
+ ? { permit: true }
188
+ : { permit: false, reason: 'ARG_MISMATCH' };
189
+ }
190
+ // --- step 4d: call_arg_field: the value of a field in the map at element i
191
+ // of the vec at argument index. Fails closed on a non-vec arg, an
192
+ // out-of-range element, a missing field, a non-map element, or a type
193
+ // mismatch between the field ScVal and the literal leaf.
194
+ if (left.kind === 'call_arg_field') {
195
+ const actual = ctx.args[left.index];
196
+ if (!actual || actual.type !== 'vec')
197
+ return { permit: false, reason: 'ARG_MISMATCH' };
198
+ const element = actual.value[left.element];
199
+ if (!element || element.type !== 'map')
200
+ return { permit: false, reason: 'ARG_MISMATCH' };
201
+ if (!Array.isArray(element.value))
202
+ return { permit: false, reason: 'ARG_MISMATCH' };
203
+ const entry = element.value.find((e) => e.key === left.field);
204
+ if (!entry)
205
+ return { permit: false, reason: 'ARG_MISMATCH' };
206
+ if (op === 'eq')
207
+ return evalArgEq(op, entry.val, right, ctx);
208
+ return evalArgOrderedCompare(op, entry.val, right);
209
+ }
178
210
  // --- step 6: AMOUNT_BOUND ---
179
211
  if (left.kind === 'amount' && op !== 'eq') {
180
212
  return evalAmountCompare(op, left.token, right, ctx);
@@ -334,6 +366,23 @@ function resolveLeaf(leaf, ctx) {
334
366
  return { type: 'symbol', value: ctx.fn };
335
367
  case 'call_arg':
336
368
  return ctx.args[leaf.index];
369
+ case 'call_arg_len':
370
+ // No direct ScVal projection: the length is an integer the comparator
371
+ // resolves against the right-hand literal. Returning undefined keeps
372
+ // the `in` membership path structurally informed (no haystack match).
373
+ return undefined;
374
+ case 'call_arg_field': {
375
+ const actual = ctx.args[leaf.index];
376
+ if (!actual || actual.type !== 'vec')
377
+ return undefined;
378
+ const element = actual.value[leaf.element];
379
+ if (!element || element.type !== 'map')
380
+ return undefined;
381
+ if (!Array.isArray(element.value))
382
+ return undefined;
383
+ const entry = element.value.find((e) => e.key === leaf.field);
384
+ return entry ? entry.val : undefined;
385
+ }
337
386
  case 'amount':
338
387
  case 'window_spent':
339
388
  case 'oracle_price':
package/dist/types.d.ts CHANGED
@@ -182,6 +182,14 @@ export type PredicateLeaf = {
182
182
  } | {
183
183
  kind: 'call_arg';
184
184
  index: number;
185
+ } | {
186
+ kind: 'call_arg_len';
187
+ index: number;
188
+ } | {
189
+ kind: 'call_arg_field';
190
+ index: number;
191
+ element: number;
192
+ field: string;
185
193
  } | {
186
194
  kind: 'amount';
187
195
  token: string;
@@ -312,6 +312,10 @@ function lowerSelector(s) {
312
312
  switch (s.kind) {
313
313
  case 'arg':
314
314
  return { kind: 'call_arg', index: s.argIndex };
315
+ case 'arg_len':
316
+ return { kind: 'call_arg_len', index: s.argIndex };
317
+ case 'arg_field':
318
+ return { kind: 'call_arg_field', index: s.argIndex, element: s.element, field: s.field };
315
319
  case 'amount':
316
320
  return { kind: 'amount', token: s.token };
317
321
  case 'window_spent':
@@ -334,13 +338,15 @@ function lowerSelector(s) {
334
338
  * selector kind to the matching `literal_*` kind. The IR compare value is a
335
339
  * raw string (i128-safe); the selector kind fixes the canonical wire type:
336
340
  * - arg -> IR scalarType (set by the recorder/parser)
341
+ * - arg_len -> u32 (vec length is a small non-negative integer)
342
+ * - arg_field -> IR scalarType (the field's recorded type)
337
343
  * - amount -> i128 (canonical Stellar token amount encoding)
338
344
  * - window_spent -> i128 (canonical amount encoding)
339
345
  * - oracle_price -> i128 (canonical price encoding)
340
346
  * - invocation_count -> u32 (counts are small non-negative integers)
341
347
  * - now / valid_until -> u64 (unix timestamps in seconds) */
342
348
  function literalFromIRCompare(c) {
343
- const scalarType = c.selector.kind === 'arg' ? c.selector.scalarType : literalScalarForSelector(c.selector.kind);
349
+ const scalarType = selectorScalarType(c.selector);
344
350
  return literalFromScalar(c.value, scalarType);
345
351
  }
346
352
  function literalScalarForSelector(kind) {
@@ -350,14 +356,16 @@ function literalScalarForSelector(kind) {
350
356
  case 'oracle_price':
351
357
  return 'i128';
352
358
  case 'invocation_count':
359
+ case 'arg_len':
353
360
  return 'u32';
354
361
  case 'now':
355
362
  case 'valid_until':
356
363
  return 'u64';
357
364
  case 'arg':
365
+ case 'arg_field':
358
366
  case 'calldata':
359
367
  case 'value':
360
- // arg -> caller handles scalarType; calldata/value -> unreachable (Path-B).
368
+ // arg / arg_field -> caller handles scalarType; calldata/value -> unreachable (Path-B).
361
369
  return 'i128';
362
370
  }
363
371
  }
@@ -385,10 +393,12 @@ function literalFromScalar(value, scalarType) {
385
393
  /** Scalar type of an IRSelector for the purpose of building literal leaves
386
394
  * (the right-hand side of `eq` / elements of an `in` haystack / elements of
387
395
  * a `literal_vec`). Mirrors `literalScalarForSelector` for OZ extensions and
388
- * uses the selector's own `scalarType` for `arg` selectors. */
396
+ * uses the selector's own `scalarType` for `arg` and `arg_field` selectors. */
389
397
  function selectorScalarType(selector) {
390
398
  if (selector.kind === 'arg')
391
399
  return selector.scalarType;
400
+ if (selector.kind === 'arg_field')
401
+ return selector.scalarType;
392
402
  return literalScalarForSelector(selector.kind);
393
403
  }
394
404
  /** Walk a condition tree and throw if any oracle_price leaf is found anywhere
@@ -462,6 +472,10 @@ function describeCondition(cond) {
462
472
  return `per-call amount comparison on ${s.token}`;
463
473
  case 'arg':
464
474
  return `argument comparison on arg ${s.argIndex}`;
475
+ case 'arg_len':
476
+ return `vec length comparison on arg ${s.argIndex}`;
477
+ case 'arg_field':
478
+ return `map field comparison on arg ${s.argIndex}.${s.field}`;
465
479
  case 'calldata':
466
480
  return 'EVM calldata comparison';
467
481
  case 'value':
@@ -477,6 +491,10 @@ function describeSelector(s) {
477
491
  switch (s.kind) {
478
492
  case 'arg':
479
493
  return `arg ${s.argIndex}`;
494
+ case 'arg_len':
495
+ return `arg_len(${s.argIndex})`;
496
+ case 'arg_field':
497
+ return `arg_field(${s.argIndex}, ${s.element}, ${s.field})`;
480
498
  case 'amount':
481
499
  return `amount(${s.token})`;
482
500
  case 'window_spent':
@@ -234,6 +234,10 @@ function describeCondition(cond) {
234
234
  return `per-call amount comparison on ${s.token} (predicate DSL)`;
235
235
  case 'arg':
236
236
  return `argument comparison on arg ${s.argIndex} (predicate DSL)`;
237
+ case 'arg_len':
238
+ return `vec length comparison on arg ${s.argIndex} (predicate DSL)`;
239
+ case 'arg_field':
240
+ return `map field comparison on arg ${s.argIndex}.${s.field} (predicate DSL)`;
237
241
  case 'calldata':
238
242
  return 'EVM calldata comparison (predicate DSL)';
239
243
  case 'value':
@@ -249,6 +253,10 @@ function describeSelector(s) {
249
253
  switch (s.kind) {
250
254
  case 'arg':
251
255
  return `arg ${s.argIndex}`;
256
+ case 'arg_len':
257
+ return `arg_len(${s.argIndex})`;
258
+ case 'arg_field':
259
+ return `arg_field(${s.argIndex}, ${s.element}, ${s.field})`;
252
260
  case 'amount':
253
261
  return `amount(${s.token})`;
254
262
  case 'window_spent':
@@ -14,6 +14,15 @@ export type IRSelector = {
14
14
  fieldIndex?: number;
15
15
  vecMode?: IRVecMode;
16
16
  scalarType: IRScalarType;
17
+ } | {
18
+ kind: 'arg_len';
19
+ argIndex: number;
20
+ } | {
21
+ kind: 'arg_field';
22
+ argIndex: number;
23
+ element: number;
24
+ field: string;
25
+ scalarType: IRScalarType;
17
26
  } | {
18
27
  kind: 'calldata';
19
28
  offset: number;
@@ -170,6 +170,15 @@ function encodeLeaf(leaf) {
170
170
  return stellar_sdk_1.xdr.ScVal.scvVec([symbol('call_fn')]);
171
171
  case 'call_arg':
172
172
  return stellar_sdk_1.xdr.ScVal.scvVec([symbol('call_arg'), stellar_sdk_1.xdr.ScVal.scvU32(leaf.index)]);
173
+ case 'call_arg_len':
174
+ return stellar_sdk_1.xdr.ScVal.scvVec([symbol('call_arg_len'), stellar_sdk_1.xdr.ScVal.scvU32(leaf.index)]);
175
+ case 'call_arg_field':
176
+ return stellar_sdk_1.xdr.ScVal.scvVec([
177
+ symbol('call_arg_field'),
178
+ stellar_sdk_1.xdr.ScVal.scvU32(leaf.index),
179
+ stellar_sdk_1.xdr.ScVal.scvU32(leaf.element),
180
+ stellar_sdk_1.xdr.ScVal.scvSymbol(leaf.field),
181
+ ]);
173
182
  case 'amount':
174
183
  return stellar_sdk_1.xdr.ScVal.scvVec([symbol('amount'), scvAddressFromStrkey(leaf.token)]);
175
184
  case 'window_spent':
@@ -187,6 +187,8 @@ function renderVecElement(leaf) {
187
187
  case 'call_contract':
188
188
  case 'call_fn':
189
189
  case 'call_arg':
190
+ case 'call_arg_len':
191
+ case 'call_arg_field':
190
192
  case 'amount':
191
193
  case 'window_spent':
192
194
  case 'now':
@@ -108,6 +108,8 @@ function renderVecElement(leaf) {
108
108
  case 'call_contract':
109
109
  case 'call_fn':
110
110
  case 'call_arg':
111
+ case 'call_arg_len':
112
+ case 'call_arg_field':
111
113
  case 'amount':
112
114
  case 'window_spent':
113
115
  case 'now':