@crediolabs/policy-synth 0.2.0 → 0.3.1

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.
Files changed (249) hide show
  1. package/README.md +4 -4
  2. package/dist/adapters/interpreter/adapter.d.ts +19 -25
  3. package/dist/adapters/interpreter/adapter.js +54 -487
  4. package/dist/errors.d.ts +1 -1
  5. package/dist/index.d.ts +1 -4
  6. package/dist/index.js +1 -4
  7. package/dist/install/build-add-context-rule.d.ts +3 -19
  8. package/dist/install/build-add-context-rule.js +3 -98
  9. package/dist/install/build-install-policy.d.ts +4 -53
  10. package/dist/install/build-install-policy.js +44 -123
  11. package/dist/install/index.d.ts +0 -2
  12. package/dist/install/index.js +0 -7
  13. package/dist/predicate/decode.d.ts +1 -1
  14. package/dist/predicate/decode.js +2 -70
  15. package/dist/predicate/encode.js +45 -229
  16. package/dist/predicate/from-json.js +1 -42
  17. package/dist/record/decode.js +3 -8
  18. package/dist/record/freshness.d.ts +17 -11
  19. package/dist/record/freshness.js +28 -18
  20. package/dist/review-card/builder.d.ts +4 -3
  21. package/dist/review-card/builder.js +18 -162
  22. package/dist/review-card/cross-check.js +2 -105
  23. package/dist/review-card/render-leaf.d.ts +4 -0
  24. package/dist/review-card/render-leaf.js +47 -0
  25. package/dist/run/index.d.ts +36 -139
  26. package/dist/run/index.js +125 -437
  27. package/dist/run/schemas.d.ts +168 -1929
  28. package/dist/run/schemas.js +40 -269
  29. package/dist/{synth → simulate}/deny-cases.d.ts +4 -8
  30. package/dist/simulate/deny-cases.js +282 -0
  31. package/dist/simulate/evaluate.d.ts +18 -0
  32. package/dist/simulate/evaluate.js +271 -0
  33. package/dist/simulate/index.d.ts +4 -0
  34. package/dist/simulate/index.js +8 -0
  35. package/dist/synth/compose-from-recording.d.ts +24 -51
  36. package/dist/synth/compose-from-recording.js +120 -282
  37. package/dist/synth/index.d.ts +0 -5
  38. package/dist/synth/index.js +0 -5
  39. package/dist/synth/synthesize-from-recording.d.ts +1 -29
  40. package/dist/synth/synthesize-from-recording.js +41 -356
  41. package/dist/types.d.ts +8 -60
  42. package/dist/types.js +5 -2
  43. package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
  44. package/dist-cjs/adapters/interpreter/adapter.js +56 -489
  45. package/dist-cjs/errors.d.ts +1 -1
  46. package/dist-cjs/index.d.ts +1 -4
  47. package/dist-cjs/index.js +1 -4
  48. package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
  49. package/dist-cjs/install/build-add-context-rule.js +1 -97
  50. package/dist-cjs/install/build-install-policy.d.ts +4 -53
  51. package/dist-cjs/install/build-install-policy.js +44 -122
  52. package/dist-cjs/install/index.d.ts +0 -2
  53. package/dist-cjs/install/index.js +2 -23
  54. package/dist-cjs/predicate/decode.d.ts +1 -1
  55. package/dist-cjs/predicate/decode.js +2 -70
  56. package/dist-cjs/predicate/encode.js +45 -229
  57. package/dist-cjs/predicate/from-json.js +1 -42
  58. package/dist-cjs/record/decode.js +3 -8
  59. package/dist-cjs/record/freshness.d.ts +17 -11
  60. package/dist-cjs/record/freshness.js +28 -18
  61. package/dist-cjs/review-card/builder.d.ts +4 -3
  62. package/dist-cjs/review-card/builder.js +23 -167
  63. package/dist-cjs/review-card/cross-check.js +7 -110
  64. package/dist-cjs/review-card/render-leaf.d.ts +4 -0
  65. package/dist-cjs/review-card/render-leaf.js +52 -0
  66. package/dist-cjs/run/index.d.ts +36 -139
  67. package/dist-cjs/run/index.js +125 -444
  68. package/dist-cjs/run/schemas.d.ts +168 -1929
  69. package/dist-cjs/run/schemas.js +41 -270
  70. package/dist-cjs/{synth → simulate}/deny-cases.d.ts +4 -8
  71. package/dist-cjs/simulate/deny-cases.js +286 -0
  72. package/dist-cjs/simulate/evaluate.d.ts +18 -0
  73. package/dist-cjs/simulate/evaluate.js +274 -0
  74. package/dist-cjs/simulate/index.d.ts +4 -0
  75. package/dist-cjs/simulate/index.js +13 -0
  76. package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
  77. package/dist-cjs/synth/compose-from-recording.js +120 -282
  78. package/dist-cjs/synth/index.d.ts +0 -5
  79. package/dist-cjs/synth/index.js +1 -11
  80. package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
  81. package/dist-cjs/synth/synthesize-from-recording.js +39 -354
  82. package/dist-cjs/types.d.ts +8 -60
  83. package/dist-cjs/types.js +6 -3
  84. package/package.json +5 -5
  85. package/src/adapters/interpreter/adapter.ts +76 -572
  86. package/src/errors.ts +0 -19
  87. package/src/index.ts +1 -4
  88. package/src/install/build-add-context-rule.ts +5 -139
  89. package/src/install/build-install-policy.ts +93 -214
  90. package/src/install/index.ts +0 -34
  91. package/src/predicate/decode.ts +2 -70
  92. package/src/predicate/encode.ts +49 -261
  93. package/src/predicate/from-json.ts +1 -43
  94. package/src/record/decode.ts +3 -8
  95. package/src/record/freshness.ts +29 -18
  96. package/src/review-card/builder.ts +19 -168
  97. package/src/review-card/cross-check.ts +3 -105
  98. package/src/review-card/render-leaf.ts +48 -0
  99. package/src/run/index.ts +144 -572
  100. package/src/run/schemas.ts +42 -291
  101. package/src/simulate/deny-cases.ts +334 -0
  102. package/src/simulate/evaluate.ts +284 -0
  103. package/src/simulate/index.ts +11 -0
  104. package/src/synth/compose-from-recording.ts +148 -347
  105. package/src/synth/index.ts +0 -13
  106. package/src/synth/synthesize-from-recording.ts +43 -456
  107. package/src/types.ts +13 -49
  108. package/dist/adapters/interpreter/index.d.ts +0 -1
  109. package/dist/adapters/interpreter/index.js +0 -2
  110. package/dist/adapters/oz/adapter.d.ts +0 -20
  111. package/dist/adapters/oz/adapter.js +0 -295
  112. package/dist/adapters/oz/index.d.ts +0 -1
  113. package/dist/adapters/oz/index.js +0 -2
  114. package/dist/codegen/compile-gate.d.ts +0 -33
  115. package/dist/codegen/compile-gate.js +0 -124
  116. package/dist/codegen/index.d.ts +0 -2
  117. package/dist/codegen/index.js +0 -8
  118. package/dist/codegen/template.d.ts +0 -18
  119. package/dist/codegen/template.js +0 -148
  120. package/dist/install/authority-overlap.d.ts +0 -134
  121. package/dist/install/authority-overlap.js +0 -0
  122. package/dist/install/build-install-predicate.d.ts +0 -96
  123. package/dist/install/build-install-predicate.js +0 -444
  124. package/dist/install/build-merge-policy.d.ts +0 -70
  125. package/dist/install/build-merge-policy.js +0 -130
  126. package/dist/install/plan-merge-policy.d.ts +0 -49
  127. package/dist/install/plan-merge-policy.js +0 -86
  128. package/dist/install/read-account-rules.d.ts +0 -100
  129. package/dist/install/read-account-rules.js +0 -283
  130. package/dist/ir/index.d.ts +0 -1
  131. package/dist/ir/index.js +0 -2
  132. package/dist/ir/types.d.ts +0 -140
  133. package/dist/ir/types.js +0 -11
  134. package/dist/mandate/index.d.ts +0 -2
  135. package/dist/mandate/index.js +0 -2
  136. package/dist/mandate/to-ir.d.ts +0 -3
  137. package/dist/mandate/to-ir.js +0 -60
  138. package/dist/mandate/types.d.ts +0 -20
  139. package/dist/mandate/types.js +0 -8
  140. package/dist/seams/index.d.ts +0 -1
  141. package/dist/seams/index.js +0 -2
  142. package/dist/seams/types.d.ts +0 -66
  143. package/dist/seams/types.js +0 -11
  144. package/dist/synth/deny-cases.js +0 -562
  145. package/dist/synth/evaluate.d.ts +0 -39
  146. package/dist/synth/evaluate.js +0 -551
  147. package/dist/synth/harness.d.ts +0 -28
  148. package/dist/synth/harness.js +0 -47
  149. package/dist/synth/minimize.d.ts +0 -4
  150. package/dist/synth/minimize.js +0 -38
  151. package/dist/synth/permit-context.d.ts +0 -15
  152. package/dist/synth/permit-context.js +0 -116
  153. package/dist/synth/predicate-literals.d.ts +0 -5
  154. package/dist/synth/predicate-literals.js +0 -25
  155. package/dist/synth/synthesize-from-mandate.d.ts +0 -20
  156. package/dist/synth/synthesize-from-mandate.js +0 -59
  157. package/dist/verify/envelope.d.ts +0 -15
  158. package/dist/verify/envelope.js +0 -22
  159. package/dist/verify/index.d.ts +0 -3
  160. package/dist/verify/index.js +0 -3
  161. package/dist/verify/simulate.d.ts +0 -31
  162. package/dist/verify/simulate.js +0 -258
  163. package/dist/verify/verify.d.ts +0 -21
  164. package/dist/verify/verify.js +0 -189
  165. package/dist-cjs/adapters/interpreter/index.d.ts +0 -1
  166. package/dist-cjs/adapters/interpreter/index.js +0 -8
  167. package/dist-cjs/adapters/oz/adapter.d.ts +0 -20
  168. package/dist-cjs/adapters/oz/adapter.js +0 -300
  169. package/dist-cjs/adapters/oz/index.d.ts +0 -1
  170. package/dist-cjs/adapters/oz/index.js +0 -8
  171. package/dist-cjs/codegen/compile-gate.d.ts +0 -33
  172. package/dist-cjs/codegen/compile-gate.js +0 -128
  173. package/dist-cjs/codegen/index.d.ts +0 -2
  174. package/dist-cjs/codegen/index.js +0 -14
  175. package/dist-cjs/codegen/template.d.ts +0 -18
  176. package/dist-cjs/codegen/template.js +0 -151
  177. package/dist-cjs/install/authority-overlap.d.ts +0 -134
  178. package/dist-cjs/install/authority-overlap.js +0 -0
  179. package/dist-cjs/install/build-install-predicate.d.ts +0 -96
  180. package/dist-cjs/install/build-install-predicate.js +0 -479
  181. package/dist-cjs/install/build-merge-policy.d.ts +0 -70
  182. package/dist-cjs/install/build-merge-policy.js +0 -134
  183. package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
  184. package/dist-cjs/install/plan-merge-policy.js +0 -90
  185. package/dist-cjs/install/read-account-rules.d.ts +0 -100
  186. package/dist-cjs/install/read-account-rules.js +0 -296
  187. package/dist-cjs/ir/index.d.ts +0 -1
  188. package/dist-cjs/ir/index.js +0 -3
  189. package/dist-cjs/ir/types.d.ts +0 -140
  190. package/dist-cjs/ir/types.js +0 -12
  191. package/dist-cjs/mandate/index.d.ts +0 -2
  192. package/dist-cjs/mandate/index.js +0 -6
  193. package/dist-cjs/mandate/to-ir.d.ts +0 -3
  194. package/dist-cjs/mandate/to-ir.js +0 -63
  195. package/dist-cjs/mandate/types.d.ts +0 -20
  196. package/dist-cjs/mandate/types.js +0 -9
  197. package/dist-cjs/seams/index.d.ts +0 -1
  198. package/dist-cjs/seams/index.js +0 -3
  199. package/dist-cjs/seams/types.d.ts +0 -66
  200. package/dist-cjs/seams/types.js +0 -12
  201. package/dist-cjs/synth/deny-cases.js +0 -568
  202. package/dist-cjs/synth/evaluate.d.ts +0 -39
  203. package/dist-cjs/synth/evaluate.js +0 -554
  204. package/dist-cjs/synth/harness.d.ts +0 -28
  205. package/dist-cjs/synth/harness.js +0 -50
  206. package/dist-cjs/synth/minimize.d.ts +0 -4
  207. package/dist-cjs/synth/minimize.js +0 -41
  208. package/dist-cjs/synth/permit-context.d.ts +0 -15
  209. package/dist-cjs/synth/permit-context.js +0 -119
  210. package/dist-cjs/synth/predicate-literals.d.ts +0 -5
  211. package/dist-cjs/synth/predicate-literals.js +0 -28
  212. package/dist-cjs/synth/synthesize-from-mandate.d.ts +0 -20
  213. package/dist-cjs/synth/synthesize-from-mandate.js +0 -62
  214. package/dist-cjs/verify/envelope.d.ts +0 -15
  215. package/dist-cjs/verify/envelope.js +0 -23
  216. package/dist-cjs/verify/index.d.ts +0 -3
  217. package/dist-cjs/verify/index.js +0 -8
  218. package/dist-cjs/verify/simulate.d.ts +0 -31
  219. package/dist-cjs/verify/simulate.js +0 -261
  220. package/dist-cjs/verify/verify.d.ts +0 -21
  221. package/dist-cjs/verify/verify.js +0 -192
  222. package/src/adapters/interpreter/index.ts +0 -8
  223. package/src/adapters/oz/adapter.ts +0 -376
  224. package/src/adapters/oz/index.ts +0 -9
  225. package/src/codegen/compile-gate.ts +0 -167
  226. package/src/codegen/index.ts +0 -17
  227. package/src/codegen/template.ts +0 -165
  228. package/src/install/authority-overlap.ts +0 -0
  229. package/src/install/build-merge-policy.ts +0 -219
  230. package/src/install/plan-merge-policy.ts +0 -133
  231. package/src/install/read-account-rules.ts +0 -376
  232. package/src/ir/index.ts +0 -13
  233. package/src/ir/types.ts +0 -132
  234. package/src/mandate/index.ts +0 -4
  235. package/src/mandate/to-ir.ts +0 -71
  236. package/src/mandate/types.ts +0 -21
  237. package/src/seams/index.ts +0 -11
  238. package/src/seams/types.ts +0 -81
  239. package/src/synth/deny-cases.ts +0 -663
  240. package/src/synth/evaluate.ts +0 -613
  241. package/src/synth/harness.ts +0 -68
  242. package/src/synth/minimize.ts +0 -48
  243. package/src/synth/permit-context.ts +0 -136
  244. package/src/synth/predicate-literals.ts +0 -27
  245. package/src/synth/synthesize-from-mandate.ts +0 -82
  246. package/src/verify/envelope.ts +0 -28
  247. package/src/verify/index.ts +0 -5
  248. package/src/verify/simulate.ts +0 -311
  249. package/src/verify/verify.ts +0 -243
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Pure function. Maps a `PredicateNode` AST to the canonical ScVal wire format:
4
4
  // - every node is a `ScVal::Vec` whose head element is the tag `ScVal::Symbol`
5
- // - children of `and` / `or` are sorted ascending by their canonical XDR bytes
5
+ // - children of `and` are sorted ascending by their canonical XDR bytes
6
6
  // - `in` haystacks are ALWAYS sorted by canonical XDR bytes (pure set
7
7
  // membership); an EXACT ordered sequence is expressed as
8
8
  // `eq(selector, literal_vec)` where the `literal_vec` element order is
@@ -17,12 +17,6 @@
17
17
  // Caps from `PREDICATE_CAPS` are enforced BEFORE returning; a cap breach throws
18
18
  // a `ToolError` with the matching error code and `severity: 'error'`.
19
19
  //
20
- // One gap remains, deliberately: the `amount` / `window_spent` leaf branches
21
- // below are dead ABI - the contract's grammar no longer has those selector
22
- // symbols, so a predicate carrying one is MALFORMED at decode. The interpreter
23
- // adapter reports them as uncovered before lowering, so the product path
24
- // cannot emit one; removing the branches (and the leaf kinds) is a separate
25
- // change.
26
20
 
27
21
  import { createHash } from 'node:crypto'
28
22
  import { Address, xdr } from '@stellar/stellar-sdk'
@@ -65,34 +59,26 @@ export function encodePredicate(node: PredicateNode): EncodedPredicate {
65
59
  )
66
60
  }
67
61
  }
68
- // Two-round confirmation costs 2 oracle reads per UNIQUE asset referenced.
69
- const oracleReads = stats.oracleAssets.size * 2
70
- if (oracleReads > PREDICATE_CAPS.MAX_ORACLE_READS) {
71
- throw capError(
72
- 'PREDICATE_ORACLE_OVER_LIMIT',
73
- `oracle reads ${oracleReads} exceed MAX_ORACLE_READS ${PREDICATE_CAPS.MAX_ORACLE_READS}`
74
- )
75
- }
76
- if (stats.oracleAssets.size > 0 && stats.nonOracleSelectorLeaves === 0) {
62
+ // Minimum constraint. A predicate with no selector leaf compares literals
63
+ // to literals, so it is trivially true or false and would permit everything
64
+ // or nothing forever. The contract refuses it at install with
65
+ // SELECTOR_LEAF_REQUIRED (216); refuse it here too so the failure is caught
66
+ // before a transaction is built.
67
+ if (stats.selectorLeaves === 0) {
77
68
  throw capError(
78
69
  'MALFORMED_PREDICATE',
79
- 'predicate constrains nothing but an oracle price: the contract refuses it at install (dsl.rs MissingNonOracleEnvelope). Pin the call itself (contract / method / argument) alongside the price bound.'
70
+ 'predicate constrains nothing: every comparison has literals on both sides. The contract refuses it at install (SELECTOR_LEAF_REQUIRED). Pin the call itself - contract, method or argument.'
80
71
  )
81
72
  }
82
73
 
83
- // --- pass 1.5: leaf-value validation (Rust `validate_scaled_ratios` + the
84
- // broader cap-set gate the contract enforces at install). Defense in depth:
85
- // the TS self-verify pipeline should reject the same shapes Rust install
86
- // refuses, so a hand-crafted predicate that simulate/verify green-lights
87
- // cannot later be refused at the on-chain install step. The checks:
88
- // - u32 fields in range (call_arg index, literal_u32 value, oracle
89
- // threshold decimals, etc.) - the contract decodes as u32
90
- // - i128 positivity where required (literal_i128 for amount/window
91
- // caps; `den`/`num` for scaled ratios) - a negative cap would
92
- // permit everything
93
- // - hex even-length (literal_bytes) - `Buffer.from(v, 'hex')` silently
94
- // drops non-hex chars, so 'zz' becomes empty bytes
95
- // - scaled-ratio num>0 && den>0 - mirrors dsl.rs:661-704
74
+ // --- pass 1.5: leaf-value validation against the cap-set gate the contract
75
+ // enforces at install. Defense in depth: the TS self-verify pipeline should
76
+ // reject the same shapes Rust install refuses, so a hand-crafted predicate
77
+ // that simulate/verify green-lights cannot later be refused at the on-chain
78
+ // install step. The checks are the u32 ranges on the argument selectors
79
+ // (`call_arg.index`, `call_arg_len.index`, `call_arg_field.index` and
80
+ // `.element`) - the contract decodes each as u32, so an out-of-range value
81
+ // would fail there instead.
96
82
  // Throws `MALFORMED_PREDICATE` so the error stays a ToolError shape.
97
83
  validateLeafValues(node)
98
84
 
@@ -114,42 +100,31 @@ interface PredicateStats {
114
100
  depth: number
115
101
  leaves: number
116
102
  inCounts: Array<{ count: number }>
117
- /** Unique assets referenced by `oracle_price` leaves - each costs 2 reads. */
118
- oracleAssets: Set<string>
119
- /** Selector leaves that are NOT `oracle_price`. The contract refuses a
120
- * predicate whose only constraint is an oracle read (dsl.rs
121
- * `validate_oracle_placement` -> MissingNonOracleEnvelope): a price bound
122
- * alone pins nothing about the call itself. Literals are operands, not
123
- * constraints, so they do not count. */
124
- nonOracleSelectorLeaves: number
103
+ /** Leaves that read something from the call under evaluation. Literals are
104
+ * operands, not constraints, so they do not count. Zero means the
105
+ * predicate constrains nothing and the contract refuses it at install. */
106
+ selectorLeaves: number
125
107
  }
126
108
 
127
109
  function computeStats(node: PredicateNode): PredicateStats {
128
110
  const inCounts: Array<{ count: number }> = []
129
- const oracleAssets = new Set<string>()
130
- const counters = { nonOracleSelectorLeaves: 0 }
131
- const { depth, leaves } = walk(node, inCounts, oracleAssets, counters, false)
111
+ const counters = { selectorLeaves: 0 }
112
+ const { depth, leaves } = walk(node, inCounts, counters)
132
113
  return {
133
114
  depth,
134
115
  leaves,
135
116
  inCounts,
136
- oracleAssets,
137
- nonOracleSelectorLeaves: counters.nonOracleSelectorLeaves,
117
+ selectorLeaves: counters.selectorLeaves,
138
118
  }
139
119
  }
140
120
 
141
121
  function walk(
142
122
  node: PredicateNode,
143
123
  inCounts: Array<{ count: number }>,
144
- oracleAssets: Set<string>,
145
- counters: { nonOracleSelectorLeaves: number },
146
- /** True once the walk is under a `not` or an `or`, where an oracle read
147
- * becomes a condition the caller can satisfy by taking the other branch. */
148
- negatedOrDisjunctive: boolean
124
+ counters: { selectorLeaves: number }
149
125
  ): { depth: number; leaves: number } {
150
126
  switch (node.op) {
151
- case 'and':
152
- case 'or': {
127
+ case 'and': {
153
128
  if (node.children.length === 0) {
154
129
  throw capError(
155
130
  'MALFORMED_PREDICATE',
@@ -159,29 +134,16 @@ function walk(
159
134
  let maxChildDepth = 0
160
135
  let totalLeaves = 0
161
136
  for (const c of node.children) {
162
- const child = walk(
163
- c,
164
- inCounts,
165
- oracleAssets,
166
- counters,
167
- negatedOrDisjunctive || node.op === 'or'
168
- )
137
+ const child = walk(c, inCounts, counters)
169
138
  if (child.depth > maxChildDepth) maxChildDepth = child.depth
170
139
  totalLeaves += child.leaves
171
140
  }
172
141
  return { depth: maxChildDepth + 1, leaves: totalLeaves }
173
142
  }
174
- case 'not': {
175
- const child = walk(node.child, inCounts, oracleAssets, counters, true)
176
- return { depth: child.depth + 1, leaves: child.leaves }
177
- }
178
143
  case 'eq':
179
- case 'lt':
180
- case 'lte':
181
- case 'gt':
182
- case 'gte': {
183
- collectOracle(node.left, oracleAssets, counters, negatedOrDisjunctive)
184
- collectOracle(node.right, oracleAssets, counters, negatedOrDisjunctive)
144
+ case 'lte': {
145
+ collectSelector(node.left, counters)
146
+ collectSelector(node.right, counters)
185
147
  return { depth: 1, leaves: leafCount(node.left) + leafCount(node.right) }
186
148
  }
187
149
  case 'in': {
@@ -192,10 +154,10 @@ function walk(
192
154
  )
193
155
  }
194
156
  inCounts.push({ count: node.haystack.length })
195
- collectOracle(node.needle, oracleAssets, counters, negatedOrDisjunctive)
157
+ collectSelector(node.needle, counters)
196
158
  let haystackLeaves = 0
197
159
  for (const h of node.haystack) {
198
- collectOracle(h, oracleAssets, counters, negatedOrDisjunctive)
160
+ collectSelector(h, counters)
199
161
  haystackLeaves += leafCount(h)
200
162
  }
201
163
  return {
@@ -218,51 +180,26 @@ function leafCount(leaf: PredicateLeaf): number {
218
180
  return 1
219
181
  }
220
182
 
221
- function collectOracle(
222
- leaf: PredicateLeaf,
223
- oracleAssets: Set<string>,
224
- counters: { nonOracleSelectorLeaves: number },
225
- negatedOrDisjunctive: boolean
226
- ): void {
227
- // literal_vec is the only nested leaf; recurse so an oracle_price buried in a
228
- // vector literal (which the lowering would forbid, but the cap-walker must
229
- // still see) is counted toward the oracle-read budget.
230
- if (leaf.kind === 'oracle_price') {
231
- if (negatedOrDisjunctive) {
232
- throw capError(
233
- 'ORACLE_LEAF_INVALID_POSITION',
234
- `oracle_price(${leaf.asset}) sits under a \`not\`/\`or\`: the contract refuses that position at install (dsl.rs validate_oracle_placement). An oracle bound must be a conjunct the call has to satisfy.`
235
- )
236
- }
237
- oracleAssets.add(leaf.asset)
238
- } else if (leaf.kind === 'literal_vec') {
239
- for (const el of leaf.elements) collectOracle(el, oracleAssets, counters, negatedOrDisjunctive)
240
- } else if (!leaf.kind.startsWith('literal_') && leaf.kind !== 'oracle_threshold') {
241
- // A threshold is the oracle compare's operand, not an independent
242
- // constraint. Counting it would let an oracle-only predicate satisfy the
243
- // non-oracle envelope with its own bound (mirrors collect_oracle_leaf in
244
- // dsl.rs, where it sits with the literals).
245
- counters.nonOracleSelectorLeaves += 1
183
+ function collectSelector(leaf: PredicateLeaf, counters: { selectorLeaves: number }): void {
184
+ // literal_vec is the only nested leaf; recurse so a selector buried in a
185
+ // vector literal is still counted. Mirrors `dsl::has_selector_leaf`.
186
+ if (leaf.kind === 'literal_vec') {
187
+ for (const el of leaf.elements) collectSelector(el, counters)
188
+ } else if (!leaf.kind.startsWith('literal_')) {
189
+ counters.selectorLeaves += 1
246
190
  }
247
191
  }
248
192
 
249
193
  function encodeNode(node: PredicateNode): xdr.ScVal {
250
194
  switch (node.op) {
251
- case 'and':
252
- case 'or': {
195
+ case 'and': {
253
196
  const encoded = node.children.map(encodeNode)
254
197
  // sort children by their canonical XDR bytes ascending.
255
198
  const sorted = sortByCanonicalBytes(encoded)
256
199
  return xdr.ScVal.scvVec([symbol(node.op), xdr.ScVal.scvVec(sorted)])
257
200
  }
258
- case 'not': {
259
- return xdr.ScVal.scvVec([symbol('not'), encodeNode(node.child)])
260
- }
261
201
  case 'eq':
262
- case 'lt':
263
- case 'lte':
264
- case 'gt':
265
- case 'gte': {
202
+ case 'lte': {
266
203
  return xdr.ScVal.scvVec([symbol(node.op), encodeLeaf(node.left), encodeLeaf(node.right)])
267
204
  }
268
205
  case 'in': {
@@ -294,49 +231,6 @@ function encodeLeaf(leaf: PredicateLeaf): xdr.ScVal {
294
231
  xdr.ScVal.scvU32(leaf.element),
295
232
  xdr.ScVal.scvSymbol(leaf.field),
296
233
  ])
297
- case 'call_arg_scaled':
298
- // Wire shape `vec[symbol, u32, i128, i128]` mirrors the Rust decoder's
299
- // expectation in dsl.rs SEL_CALL_ARG_SCALED. `scvI128FromDecimal` is
300
- // the same range-checked helper used by `literal_i128`; an out-of-range
301
- // num/den is refused at encode time (rather than at install on chain).
302
- return xdr.ScVal.scvVec([
303
- symbol('call_arg_scaled'),
304
- xdr.ScVal.scvU32(leaf.index),
305
- scvI128FromDecimal(leaf.num),
306
- scvI128FromDecimal(leaf.den),
307
- ])
308
- case 'amount':
309
- return xdr.ScVal.scvVec([symbol('amount'), scvAddressFromStrkey(leaf.token)])
310
- case 'window_spent':
311
- return xdr.ScVal.scvVec([
312
- symbol('window_spent'),
313
- scvAddressFromStrkey(leaf.token),
314
- scvU64FromValue(leaf.windowSeconds),
315
- ])
316
- case 'now':
317
- return xdr.ScVal.scvVec([symbol('now')])
318
- case 'valid_until':
319
- // The interpreter refuses this leaf at install: it never sources
320
- // valid_until_ledger, so a policy built on it would deny forever.
321
- // Expiry belongs to the smart account, through the context rule's
322
- // validUntilLedger. Refuse at synthesis so the author finds out here
323
- // rather than from an install that always fails.
324
- throw new Error(
325
- 'valid_until is not a usable predicate leaf: the interpreter never sources it and ' +
326
- 'refuses it at install. Put expiry on the context rule (validUntilLedger) instead.'
327
- )
328
- case 'invocation_count_in_window':
329
- return xdr.ScVal.scvVec([symbol('invocation_count'), scvU64FromValue(leaf.windowSecs)])
330
- case 'oracle_price':
331
- return xdr.ScVal.scvVec([symbol('oracle_price'), scvAddressFromStrkey(leaf.asset)])
332
- case 'oracle_threshold':
333
- // Arity 3, matching SEL_ORACLE_THRESHOLD in dsl.rs. The declared basis
334
- // travels with the value so the contract never has to assume one.
335
- return xdr.ScVal.scvVec([
336
- symbol('oracle_threshold'),
337
- scvI128FromDecimal(leaf.value),
338
- xdr.ScVal.scvU32(leaf.decimals),
339
- ])
340
234
  case 'literal_address':
341
235
  return scvAddressFromStrkey(leaf.value)
342
236
  case 'literal_i128':
@@ -345,10 +239,6 @@ function encodeLeaf(leaf: PredicateLeaf): xdr.ScVal {
345
239
  return xdr.ScVal.scvSymbol(leaf.value)
346
240
  case 'literal_u32':
347
241
  return xdr.ScVal.scvU32(leaf.value)
348
- case 'literal_u64':
349
- return scvU64FromValue(leaf.value)
350
- case 'literal_bytes':
351
- return xdr.ScVal.scvBytes(Buffer.from(leaf.value, 'hex'))
352
242
  case 'literal_vec':
353
243
  // Bare ScVal::Vec of element encodings - order is preserved verbatim
354
244
  // because the order IS the semantic (exact ordered sequence equality).
@@ -370,12 +260,6 @@ function scvAddressFromStrkey(strkey: string): xdr.ScVal {
370
260
  return xdr.ScVal.scvAddress(Address.fromString(strkey).toScAddress())
371
261
  }
372
262
 
373
- function scvU64FromValue(value: number | string): xdr.ScVal {
374
- // The Uint64 (UnsignedHyper) constructor accepts string | bigint | number;
375
- // string is safest for values > 2^53.
376
- return xdr.ScVal.scvU64(new xdr.Uint64(String(value)))
377
- }
378
-
379
263
  /** Build `ScVal::I128(Int128Parts{hi, lo})` from a signed decimal string.
380
264
  * `Int128Parts` encodes the value as `(hi << 64) + lo` with `hi` a SIGNED
381
265
  * 64-bit int and `lo` an UNSIGNED 64-bit int (this is NOT signed-magnitude).
@@ -394,12 +278,8 @@ function scvI128FromDecimal(decimal: string): xdr.ScVal {
394
278
  `literal_i128 value ${decimal} is outside the Int128 range`
395
279
  )
396
280
  }
397
- if (lo < 0n || lo > UINT64_MAX) {
398
- throw capError(
399
- 'MALFORMED_PREDICATE',
400
- `literal_i128 value ${decimal} has an invalid Int128Parts.lo`
401
- )
402
- }
281
+ // `lo` needs no range check: masking with UINT64_MAX puts it in
282
+ // [0, 2^64-1] for every v, negative included.
403
283
  return xdr.ScVal.scvI128(
404
284
  new xdr.Int128Parts({
405
285
  hi: new xdr.Int64(hi),
@@ -423,20 +303,14 @@ function capError(code: ToolError['code'], message: string): ToolError {
423
303
  // u32 boundary - the same constant the contract decodes with. A value above
424
304
  // this either overflows during encode or is refused at install.
425
305
  const U32_MAX = 4294967295
426
- // The maximum decimal basis an oracle threshold can declare (mirrors
427
- // `MAX_ORACLE_THRESHOLD_DECIMALS` in dsl.rs). A value above this is refused
428
- // at install with `ORACLE_PARAMS_OUT_OF_RANGE`.
429
- const MAX_ORACLE_THRESHOLD_DECIMALS = 18
430
306
 
431
- /** Walk a `PredicateNode` and fail-closed on any leaf whose cap-set value the
432
- * contract would refuse at install. Mirrors `validate_scaled_ratios` in
433
- * dsl.rs:661-704 plus the broader cap-set gate (`literal_u32`, `literal_i128`
434
- * positivity, `literal_bytes` hex even-length, `oracle_threshold` decimals
435
- * range, `call_arg_scaled` positive-ratio). Defense in depth so the TS
436
- * self-verify pipeline rejects the same shapes Rust install already
437
- * refuses - a hand-crafted predicate that simulate/verify green-lights must
438
- * NOT be installable. Throws `MALFORMED_PREDICATE` so the envelope shapes
439
- * it into a ToolError. */
307
+ /** Walk a `PredicateNode` and fail-closed on any leaf whose value the contract
308
+ * would refuse at install: the argument selectors carry u32 indices, so a
309
+ * value outside that range is refused here rather than on chain. Defense in
310
+ * depth so the TS self-verify pipeline rejects the same shapes Rust install
311
+ * already refuses - a hand-crafted predicate that simulate/verify
312
+ * green-lights must NOT be installable. Throws `MALFORMED_PREDICATE` so the
313
+ * envelope shapes it into a ToolError. */
440
314
  function validateLeafValues(node: PredicateNode): void {
441
315
  function walkLeaf(leaf: PredicateLeaf, path: string): void {
442
316
  switch (leaf.kind) {
@@ -458,108 +332,22 @@ function validateLeafValues(node: PredicateNode): void {
458
332
  throw malformed(`call_arg_field.element out of u32 range at ${path}`)
459
333
  }
460
334
  return
461
- case 'call_arg_scaled': {
462
- if (!Number.isInteger(leaf.index) || leaf.index < 0 || leaf.index > U32_MAX) {
463
- throw malformed(`call_arg_scaled.index out of u32 range at ${path}`)
464
- }
465
- // num / den are i128 on chain, decimal strings on the wire. The
466
- // contract refuses `den == 0` and `num <= 0` / `den <= 0` at install
467
- // (dsl.rs:664-672); mirror that here so a future regression in
468
- // `validate_scaled_ratios` cannot let a divide-by-zero policy reach
469
- // the wire. BigInt throws on non-numeric strings -> malformed.
470
- let num: bigint
471
- let den: bigint
472
- try {
473
- num = BigInt(leaf.num)
474
- den = BigInt(leaf.den)
475
- } catch {
476
- throw malformed(`call_arg_scaled.num/den not a decimal integer at ${path}`)
477
- }
478
- if (num <= 0n) throw malformed(`call_arg_scaled.num must be > 0 at ${path}`)
479
- if (den <= 0n) throw malformed(`call_arg_scaled.den must be > 0 at ${path}`)
480
- return
481
- }
482
- case 'literal_u32':
483
- if (!Number.isInteger(leaf.value) || leaf.value < 0 || leaf.value > U32_MAX) {
484
- throw malformed(`literal_u32.value out of u32 range at ${path}`)
485
- }
486
- return
487
- case 'literal_i128':
488
- // literal_i128 is signed; the contract allows negatives (i128
489
- // arithmetic), but caps on a positive quantity (amount / window
490
- // bound) should never be negative - a negative cap is silently
491
- // satisfied by every non-negative amount. The contract gate is
492
- // already on the leaf's ROLE (amount vs equality) not the value;
493
- // here we mirror the value-only invariant the cap-set gate enforces
494
- // by refusing the syntactic shape that would clearly be a bug
495
- // (literal_i128 as a CAP with a leading `-` on a non-equality).
496
- // We do not gate equality i128 - `literal_i128` as an address-by-
497
- // equality is fine (it is just a constant).
498
- // The value itself is always accepted; the structural check
499
- // (non-negative for an amount / window bound) is left to the
500
- // caller-built predicate, not the encoder.
501
- return
502
- case 'literal_bytes':
503
- // Hex even-length: a non-hex char silently drops, and an odd
504
- // length yields a half-byte Buffer. The contract decodes with a
505
- // strict hex parser and refuses anything that is not even-length
506
- // hex; mirror that here.
507
- if (!/^[0-9a-fA-F]*$/.test(leaf.value) || leaf.value.length % 2 !== 0) {
508
- throw malformed(`literal_bytes.value must be even-length hex at ${path}`)
509
- }
510
- return
511
- case 'oracle_threshold':
512
- if (
513
- !Number.isInteger(leaf.decimals) ||
514
- leaf.decimals < 0 ||
515
- leaf.decimals > MAX_ORACLE_THRESHOLD_DECIMALS
516
- ) {
517
- throw malformed(
518
- `oracle_threshold.decimals out of range (0..${MAX_ORACLE_THRESHOLD_DECIMALS}) at ${path}`
519
- )
520
- }
521
- return
522
- case 'literal_vec':
523
- leaf.elements.forEach((e, i) => {
524
- walkLeaf(e, `${path}.elements[${i}]`)
525
- })
526
- return
527
- // Selector and other leaves carry no cap-set values; the call_arg
528
- // branches above cover indices, the literal branches cover typed
529
- // constants. amount / window_spent / invocation_count / now /
530
- // valid_until / call_contract / call_fn / literal_address /
531
- // literal_symbol / literal_u64 / oracle_price are all value-free
532
- // at this gate.
533
- case 'amount':
534
- case 'window_spent':
535
- case 'invocation_count_in_window':
536
- case 'now':
537
- case 'valid_until':
538
335
  case 'call_contract':
539
336
  case 'call_fn':
540
337
  case 'literal_address':
541
338
  case 'literal_symbol':
542
- case 'literal_u64':
543
- case 'oracle_price':
544
339
  return
545
340
  }
546
341
  }
547
342
  function walkNode(n: PredicateNode, path: string): void {
548
343
  switch (n.op) {
549
344
  case 'and':
550
- case 'or':
551
345
  n.children.forEach((c, i) => {
552
346
  walkNode(c, `${path}.children[${i}]`)
553
347
  })
554
348
  return
555
- case 'not':
556
- walkNode(n.child, `${path}.child`)
557
- return
558
349
  case 'eq':
559
- case 'lt':
560
350
  case 'lte':
561
- case 'gt':
562
- case 'gte':
563
351
  walkLeaf(n.left, `${path}.left`)
564
352
  walkLeaf(n.right, `${path}.right`)
565
353
  return
@@ -19,7 +19,6 @@ export function jsonToAst(value: unknown): PredicateNode {
19
19
  const v = value as {
20
20
  op?: unknown
21
21
  children?: unknown
22
- child?: unknown
23
22
  left?: unknown
24
23
  right?: unknown
25
24
  needle?: unknown
@@ -27,15 +26,9 @@ export function jsonToAst(value: unknown): PredicateNode {
27
26
  }
28
27
  switch (v.op) {
29
28
  case 'and':
30
- case 'or':
31
- return { op: v.op, children: arrayOf(v.children, jsonToAst) }
32
- case 'not':
33
- return { op: 'not', child: jsonToAst(v.child) }
29
+ return { op: 'and', children: arrayOf(v.children, jsonToAst) }
34
30
  case 'eq':
35
- case 'lt':
36
31
  case 'lte':
37
- case 'gt':
38
- case 'gte':
39
32
  return { op: v.op, left: jsonToLeaf(v.left), right: jsonToLeaf(v.right) }
40
33
  case 'in':
41
34
  return { op: 'in', needle: jsonToLeaf(v.needle), haystack: arrayOf(v.haystack, jsonToLeaf) }
@@ -55,42 +48,11 @@ function jsonToLeaf(value: unknown): PredicateLeaf {
55
48
  switch (v.kind) {
56
49
  case 'call_contract':
57
50
  case 'call_fn':
58
- case 'now':
59
- case 'valid_until':
60
51
  return { kind: v.kind }
61
52
  case 'call_arg':
62
53
  return { kind: 'call_arg', index: numberField(v, 'index') }
63
54
  case 'call_arg_len':
64
55
  return { kind: 'call_arg_len', index: numberField(v, 'index') }
65
- // num/den are strings, not numbers: they are i128 on chain, and a JSON
66
- // number would silently lose precision past 2^53 before the encoder
67
- // ever saw the value.
68
- case 'call_arg_scaled':
69
- return {
70
- kind: 'call_arg_scaled',
71
- index: numberField(v, 'index'),
72
- num: stringField(v, 'num'),
73
- den: stringField(v, 'den'),
74
- }
75
- case 'call_arg_field':
76
- return {
77
- kind: 'call_arg_field',
78
- index: numberField(v, 'index'),
79
- element: numberField(v, 'element'),
80
- field: stringField(v, 'field'),
81
- }
82
- case 'amount':
83
- return { kind: 'amount', token: stringField(v, 'token') }
84
- case 'window_spent':
85
- return {
86
- kind: 'window_spent',
87
- token: stringField(v, 'token'),
88
- windowSeconds: numberField(v, 'windowSeconds'),
89
- }
90
- case 'invocation_count_in_window':
91
- return { kind: 'invocation_count_in_window', windowSecs: numberField(v, 'windowSecs') }
92
- case 'oracle_price':
93
- return { kind: 'oracle_price', asset: stringField(v, 'asset') }
94
56
  case 'literal_address':
95
57
  return { kind: 'literal_address', value: stringField(v, 'value') }
96
58
  case 'literal_i128':
@@ -99,10 +61,6 @@ function jsonToLeaf(value: unknown): PredicateLeaf {
99
61
  return { kind: 'literal_symbol', value: stringField(v, 'value') }
100
62
  case 'literal_u32':
101
63
  return { kind: 'literal_u32', value: numberField(v, 'value') }
102
- case 'literal_u64':
103
- return { kind: 'literal_u64', value: stringField(v, 'value') }
104
- case 'literal_bytes':
105
- return { kind: 'literal_bytes', value: stringField(v, 'value') }
106
64
  case 'literal_vec':
107
65
  return { kind: 'literal_vec', elements: arrayOf(v.elements, jsonToLeaf) }
108
66
  default:
@@ -453,14 +453,9 @@ function recordInvocation(
453
453
  // parseConfidence denominator isn't inflated by repeat calls), but each
454
454
  // unrecognised invocation is recorded separately so the numerator
455
455
  // reflects every fail-closed hit.
456
- if (knownSet.has(contract)) {
457
- if (!knownContractSet.has(contract)) {
458
- knownContractSet.add(contract)
459
- knownOut.push(contract)
460
- }
461
- return
462
- }
463
- if (identifyProtocol(contract, fn, args, network ?? undefined)) {
456
+ // `||` short-circuits, so `identifyProtocol` is still only consulted when the
457
+ // contract is not already in `knownSet` - the same call order as before.
458
+ if (knownSet.has(contract) || identifyProtocol(contract, fn, args, network ?? undefined)) {
464
459
  if (!knownContractSet.has(contract)) {
465
460
  knownContractSet.add(contract)
466
461
  knownOut.push(contract)
@@ -52,17 +52,23 @@ export function isBelowThreshold(c: ParseConfidence): boolean {
52
52
  * under-confidence recording.
53
53
  *
54
54
  * The remediation text branches on which diagnostic bucket is non-empty:
55
- * - unknownContracts only -> user MUST supply an ABI (or re-capture
56
- * against a known protocol version). The contract is real, the
57
- * recorder just cannot decode it.
58
- * - opaqueScVals only -> the recorder encountered a value shape it
59
- * should support but did not decode. The user cannot supply an ABI
60
- * to fix a decoder bug; the guidance explicitly says so and points at
61
- * re-running after a tool upgrade / reporting the path.
62
- * - both -> list both diagnostics AND chain the right remediation for
63
- * each (the user supplies an ABI AND reports the decoder gap).
64
- * - neither (the "denom === 0" path) -> unchanged from the pre-fix
65
- * text; the user did not actually hit a code-level barrier. */
55
+ * - unknownContracts only -> the contract is not in the compiled-in
56
+ * registry. NOT "the contract has no interface": most Soroban
57
+ * contracts publish a typed spec on chain, this package just never
58
+ * reads one. The reason code stays `no-abi` because it is a published
59
+ * enum value, so the TEXT has to carry the correction.
60
+ * - opaqueScVals only -> the recorder met a value shape it should
61
+ * support but did not decode. That is a decoder bug, and the guidance
62
+ * says so rather than implying the caller can fix it.
63
+ * - both -> list both diagnostics and chain the right remediation.
64
+ * - neither (the "denom === 0" path) -> the caller did not actually hit
65
+ * a code-level barrier.
66
+ *
67
+ * Do NOT tell the caller to supply an ABI. There is no input for one:
68
+ * recognition runs off the compiled-in registry plus a known-contract set
69
+ * that no public tool boundary exposes. The old text named an action
70
+ * nobody could take and sent users looking for a file they do not have and
71
+ * would not need. */
66
72
  export function buildLowConfidenceQuestion(c: ParseConfidence): string {
67
73
  const hasUnknown = c.unknownContracts.length > 0
68
74
  const hasOpaque = c.opaqueScVals.length > 0
@@ -75,18 +81,23 @@ export function buildLowConfidenceQuestion(c: ParseConfidence): string {
75
81
  }
76
82
  const why = reasons.length === 0 ? 'no diagnostic reason available' : reasons.join('; ')
77
83
  const header = `Recording refused: parseConfidence ${c.overall.toFixed(3)} is below the threshold ${c.thresholdUsed.toFixed(3)}. Diagnostic: ${why}.`
78
- // The two remediation branches are explicit so the user (or the agent)
79
- // can dispatch on the verb. The "unknown contract" branch is the only
80
- // one that asks for an ABI; the "opaque ScVal" branch asks the user to
81
- // report the path and re-run after a tool upgrade.
84
+ // The remediation branches are explicit so the user (or the agent) can
85
+ // dispatch on the verb. Neither branch asks for an ABI: there is no input
86
+ // that accepts one, and for the unknown-contract branch the contract's
87
+ // own spec is usually on chain already - it is this package that does not
88
+ // read it.
89
+ const unknownAdvice =
90
+ `The contract is not in this package's built-in registry - "no-abi" means unrecognised here, not that the contract lacks an interface. ` +
91
+ `You cannot supply one: the registry is compiled in. Either build the policy without deriving it from this transaction, or re-capture against a protocol the registry covers. ` +
92
+ `Passing confidenceOverride accepts the recording as it stands, including the arguments this tool could not attribute - do that only if you have checked them yourself.`
82
93
  if (hasUnknown && hasOpaque) {
83
94
  return (
84
- `${header} Supply an ABI for the unknown contract(s) or re-capture the transaction against a known protocol version; ` +
85
- `also file the opaque ScVal path against the recorder (decoder limitation) and re-run record_transaction after a tool upgrade.`
95
+ `${header} ${unknownAdvice} Separately, file the opaque ScVal path against the recorder (a decoder limitation) ` +
96
+ `and re-run record_transaction after a tool upgrade.`
86
97
  )
87
98
  }
88
99
  if (hasUnknown) {
89
- return `${header} Supply an ABI for the unknown contract(s) or re-capture the transaction against a known protocol version, then re-run record_transaction.`
100
+ return `${header} ${unknownAdvice}`
90
101
  }
91
102
  if (hasOpaque) {
92
103
  return `${header} This is a recorder decoder limitation (the value shape is supported in principle but not yet decoded) - the agent cannot fix it by supplying an ABI. Report the opaque ScVal path above against the recorder and re-run record_transaction after a tool upgrade.`