@crediolabs/policy-synth 0.1.18 → 0.3.0

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 (159) hide show
  1. package/README.md +6 -5
  2. package/dist/adapters/interpreter/adapter.d.ts +19 -25
  3. package/dist/adapters/interpreter/adapter.js +54 -487
  4. package/dist/adapters/interpreter/index.d.ts +1 -1
  5. package/dist/adapters/interpreter/index.js +1 -1
  6. package/dist/adapters/oz/adapter.js +1 -15
  7. package/dist/errors.d.ts +1 -1
  8. package/dist/index.d.ts +1 -4
  9. package/dist/index.js +1 -4
  10. package/dist/install/build-add-context-rule.d.ts +3 -11
  11. package/dist/install/build-add-context-rule.js +2 -97
  12. package/dist/install/build-install-policy.d.ts +4 -53
  13. package/dist/install/build-install-policy.js +44 -123
  14. package/dist/ir/index.d.ts +1 -1
  15. package/dist/ir/types.d.ts +7 -76
  16. package/dist/ir/types.js +0 -2
  17. package/dist/predicate/decode.d.ts +1 -1
  18. package/dist/predicate/decode.js +2 -70
  19. package/dist/predicate/encode.js +45 -229
  20. package/dist/predicate/from-json.js +1 -42
  21. package/dist/record/decode.js +3 -8
  22. package/dist/review-card/builder.d.ts +4 -3
  23. package/dist/review-card/builder.js +18 -162
  24. package/dist/review-card/cross-check.js +2 -105
  25. package/dist/review-card/render-leaf.d.ts +4 -0
  26. package/dist/review-card/render-leaf.js +47 -0
  27. package/dist/run/index.d.ts +35 -53
  28. package/dist/run/index.js +115 -156
  29. package/dist/run/schemas.d.ts +168 -1650
  30. package/dist/run/schemas.js +36 -199
  31. package/dist/seams/types.d.ts +0 -2
  32. package/dist/simulate/deny-cases.d.ts +26 -0
  33. package/dist/simulate/deny-cases.js +282 -0
  34. package/dist/simulate/evaluate.d.ts +18 -0
  35. package/dist/simulate/evaluate.js +271 -0
  36. package/dist/simulate/index.d.ts +4 -0
  37. package/dist/simulate/index.js +8 -0
  38. package/dist/synth/compose-from-recording.d.ts +24 -51
  39. package/dist/synth/compose-from-recording.js +120 -282
  40. package/dist/synth/deny-cases.d.ts +5 -7
  41. package/dist/synth/deny-cases.js +23 -373
  42. package/dist/synth/evaluate.d.ts +0 -21
  43. package/dist/synth/evaluate.js +56 -336
  44. package/dist/synth/index.d.ts +0 -5
  45. package/dist/synth/index.js +0 -5
  46. package/dist/synth/permit-context.d.ts +2 -9
  47. package/dist/synth/permit-context.js +1 -82
  48. package/dist/synth/synthesize-from-recording.d.ts +1 -29
  49. package/dist/synth/synthesize-from-recording.js +41 -356
  50. package/dist/types.d.ts +8 -60
  51. package/dist/types.js +5 -2
  52. package/dist/verify/simulate.d.ts +1 -14
  53. package/dist/verify/simulate.js +2 -88
  54. package/dist/verify/verify.d.ts +1 -7
  55. package/dist/verify/verify.js +0 -8
  56. package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
  57. package/dist-cjs/adapters/interpreter/adapter.js +56 -489
  58. package/dist-cjs/adapters/interpreter/index.d.ts +1 -1
  59. package/dist-cjs/adapters/interpreter/index.js +1 -2
  60. package/dist-cjs/adapters/oz/adapter.js +1 -15
  61. package/dist-cjs/errors.d.ts +1 -1
  62. package/dist-cjs/index.d.ts +1 -4
  63. package/dist-cjs/index.js +1 -4
  64. package/dist-cjs/install/build-add-context-rule.d.ts +3 -11
  65. package/dist-cjs/install/build-add-context-rule.js +1 -96
  66. package/dist-cjs/install/build-install-policy.d.ts +4 -53
  67. package/dist-cjs/install/build-install-policy.js +44 -122
  68. package/dist-cjs/ir/index.d.ts +1 -1
  69. package/dist-cjs/ir/types.d.ts +7 -76
  70. package/dist-cjs/ir/types.js +0 -2
  71. package/dist-cjs/predicate/decode.d.ts +1 -1
  72. package/dist-cjs/predicate/decode.js +2 -70
  73. package/dist-cjs/predicate/encode.js +45 -229
  74. package/dist-cjs/predicate/from-json.js +1 -42
  75. package/dist-cjs/record/decode.js +3 -8
  76. package/dist-cjs/review-card/builder.d.ts +4 -3
  77. package/dist-cjs/review-card/builder.js +23 -167
  78. package/dist-cjs/review-card/cross-check.js +7 -110
  79. package/dist-cjs/review-card/render-leaf.d.ts +4 -0
  80. package/dist-cjs/review-card/render-leaf.js +52 -0
  81. package/dist-cjs/run/index.d.ts +35 -53
  82. package/dist-cjs/run/index.js +116 -162
  83. package/dist-cjs/run/schemas.d.ts +168 -1650
  84. package/dist-cjs/run/schemas.js +37 -200
  85. package/dist-cjs/seams/types.d.ts +0 -2
  86. package/dist-cjs/simulate/deny-cases.d.ts +26 -0
  87. package/dist-cjs/simulate/deny-cases.js +286 -0
  88. package/dist-cjs/simulate/evaluate.d.ts +18 -0
  89. package/dist-cjs/simulate/evaluate.js +274 -0
  90. package/dist-cjs/simulate/index.d.ts +4 -0
  91. package/dist-cjs/simulate/index.js +13 -0
  92. package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
  93. package/dist-cjs/synth/compose-from-recording.js +120 -282
  94. package/dist-cjs/synth/deny-cases.d.ts +5 -7
  95. package/dist-cjs/synth/deny-cases.js +23 -374
  96. package/dist-cjs/synth/evaluate.d.ts +0 -21
  97. package/dist-cjs/synth/evaluate.js +57 -337
  98. package/dist-cjs/synth/index.d.ts +0 -5
  99. package/dist-cjs/synth/index.js +1 -11
  100. package/dist-cjs/synth/permit-context.d.ts +2 -9
  101. package/dist-cjs/synth/permit-context.js +1 -82
  102. package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
  103. package/dist-cjs/synth/synthesize-from-recording.js +39 -354
  104. package/dist-cjs/types.d.ts +8 -60
  105. package/dist-cjs/types.js +6 -3
  106. package/dist-cjs/verify/simulate.d.ts +1 -14
  107. package/dist-cjs/verify/simulate.js +2 -88
  108. package/dist-cjs/verify/verify.d.ts +1 -7
  109. package/dist-cjs/verify/verify.js +0 -8
  110. package/package.json +4 -4
  111. package/src/adapters/interpreter/adapter.ts +76 -572
  112. package/src/errors.ts +0 -19
  113. package/src/index.ts +1 -4
  114. package/src/install/build-add-context-rule.ts +4 -127
  115. package/src/install/build-install-policy.ts +93 -214
  116. package/src/predicate/decode.ts +2 -70
  117. package/src/predicate/encode.ts +49 -261
  118. package/src/predicate/from-json.ts +1 -43
  119. package/src/record/decode.ts +3 -8
  120. package/src/review-card/builder.ts +19 -168
  121. package/src/review-card/cross-check.ts +3 -105
  122. package/src/review-card/render-leaf.ts +48 -0
  123. package/src/run/index.ts +130 -186
  124. package/src/run/schemas.ts +38 -214
  125. package/src/simulate/deny-cases.ts +334 -0
  126. package/src/simulate/evaluate.ts +284 -0
  127. package/src/simulate/index.ts +11 -0
  128. package/src/synth/compose-from-recording.ts +148 -347
  129. package/src/synth/index.ts +0 -13
  130. package/src/synth/synthesize-from-recording.ts +43 -456
  131. package/src/types.ts +13 -49
  132. package/dist/install/build-install-predicate.d.ts +0 -96
  133. package/dist/install/build-install-predicate.js +0 -444
  134. package/dist-cjs/install/build-install-predicate.d.ts +0 -96
  135. package/dist-cjs/install/build-install-predicate.js +0 -479
  136. package/src/adapters/interpreter/index.ts +0 -8
  137. package/src/adapters/oz/adapter.ts +0 -376
  138. package/src/adapters/oz/index.ts +0 -9
  139. package/src/codegen/compile-gate.ts +0 -167
  140. package/src/codegen/index.ts +0 -17
  141. package/src/codegen/template.ts +0 -165
  142. package/src/ir/index.ts +0 -13
  143. package/src/ir/types.ts +0 -132
  144. package/src/mandate/index.ts +0 -4
  145. package/src/mandate/to-ir.ts +0 -71
  146. package/src/mandate/types.ts +0 -21
  147. package/src/seams/index.ts +0 -11
  148. package/src/seams/types.ts +0 -81
  149. package/src/synth/deny-cases.ts +0 -663
  150. package/src/synth/evaluate.ts +0 -613
  151. package/src/synth/harness.ts +0 -68
  152. package/src/synth/minimize.ts +0 -48
  153. package/src/synth/permit-context.ts +0 -136
  154. package/src/synth/predicate-literals.ts +0 -27
  155. package/src/synth/synthesize-from-mandate.ts +0 -82
  156. package/src/verify/envelope.ts +0 -28
  157. package/src/verify/index.ts +0 -5
  158. package/src/verify/simulate.ts +0 -311
  159. package/src/verify/verify.ts +0 -243
package/src/errors.ts CHANGED
@@ -28,7 +28,6 @@ export type ErrorCode =
28
28
  | 'PREDICATE_TOO_DEEP'
29
29
  | 'TOO_MANY_LEAVES'
30
30
  | 'IN_OPERAND_LIMIT'
31
- | 'PREDICATE_ORACLE_OVER_LIMIT'
32
31
  | 'POLICY_CAP_EXCEEDED'
33
32
  | 'WASM_TOO_LARGE'
34
33
  // --- interpreter-side denies (canonical names; matches Rust DenyCode) ---
@@ -44,24 +43,6 @@ export type ErrorCode =
44
43
  | 'UNSUPPORTED_NODE'
45
44
  | 'STATEFUL_BOUND'
46
45
  | 'NOT_IN_ALLOWLIST'
47
- | 'FREQUENCY'
48
- // A `call_arg >= call_arg_scaled(..)` slippage floor was not met. Distinct
49
- // from the generic ARG_MISMATCH/STATEFUL_BOUND so a violated swap floor
50
- // reads as itself on the review card.
51
- | 'SLIPPAGE_FLOOR'
52
- // --- oracle denies (fatal evaluator errors) ---
53
- | 'ORACLE_STALE'
54
- | 'ORACLE_MISSING'
55
- | 'ORACLE_NO_CONFIRMATION'
56
- | 'ORACLE_DEVIATION_EXCEEDED'
57
- | 'ORACLE_MALFORMED_HISTORY'
58
- | 'ORACLE_FINGERPRINT_DRIFT'
59
- | 'ORACLE_PAUSED'
60
- | 'ORACLE_LEAF_INVALID_POSITION'
61
- | 'ORACLE_PARAMS_OUT_OF_RANGE'
62
- | 'ORACLE_DECIMALS_MISMATCH'
63
- | 'ORACLE_THRESHOLD_DECIMALS_OUT_OF_RANGE'
64
- | 'ORACLE_THRESHOLD_BASIS_REQUIRED'
65
46
  // --- escape-hatch compile gate ---
66
47
  | 'COMPILE_OK'
67
48
  | 'COMPILE_GATE_FAILED'
package/src/index.ts CHANGED
@@ -1,11 +1,8 @@
1
- export * from './adapters/oz/index.ts'
2
1
  export * from './errors.ts'
3
- export * from './ir/index.ts'
4
- export * from './mandate/index.ts'
5
2
  export * from './predicate/index.ts'
6
3
  export * from './record/index.ts'
7
4
  export * from './registry/index.ts'
8
5
  export * from './review-card/index.ts'
9
- export * from './seams/index.ts'
6
+ export * from './simulate/index.ts'
10
7
  export * from './synth/index.ts'
11
8
  export * from './types.ts'
@@ -52,8 +52,8 @@ import { Address, xdr } from '@stellar/stellar-sdk'
52
52
  import type { ToolError } from '../errors.ts'
53
53
  import {
54
54
  type ContextRuleDraft,
55
+ GRAMMAR_VERSION,
55
56
  OZ_LIMITS,
56
- type OZPrimitiveConfig,
57
57
  type PolicyRef,
58
58
  type SignerDraft,
59
59
  } from '../types.ts'
@@ -66,8 +66,8 @@ export interface BuildAddContextRuleArgs {
66
66
  * admin session. The deployer signs the install call via the admin rule
67
67
  * already on the account, which is separate from this list. */
68
68
  signers: SignerDraft[]
69
- /** The policy(ies) to attach to the new rule. Only `interpreter` refs
70
- * reach the wire today; `oz_builtin` is reserved for Phase 06 follow-ups. */
69
+ /** The policy(ies) to attach to the new rule. `interpreter` is the only
70
+ * kind: the OpenZeppelin built-in backend was removed. */
71
71
  policies: PolicyRef[]
72
72
  /** Per-policy install nonce; 1 for a fresh install. */
73
73
  installNonce: number
@@ -77,20 +77,12 @@ export interface BuildAddContextRuleArgs {
77
77
  encodedPredicate: string
78
78
  /** Hex sha256 of the canonical predicate XDR bytes. */
79
79
  predicateHash: string
80
- /** Per-policy oracle overrides. Absent on any key uses the wasm default. */
81
- oracleParams?: {
82
- maxStalenessSeconds?: number
83
- maxDeviationBps?: number
84
- /** Bound between the primary and secondary feeds. Tighten-only against
85
- * the wasm default, like the other two. */
86
- maxCrossFeedDeviationBps?: number
87
- }
88
80
  /** Hard pin to the interpreter's wasm grammar. Refusing here fails closed
89
81
  * before the chain does; the contract will refuse a mismatch again. */
90
82
  grammarVersion?: number
91
83
  }
92
84
 
93
- export const DEFAULT_GRAMMAR_VERSION = 1 as const
85
+ export const DEFAULT_GRAMMAR_VERSION = GRAMMAR_VERSION
94
86
 
95
87
  /** The verb `add_context_rule` takes on the wire. */
96
88
  export const ADD_CONTEXT_RULE_SYMBOL = 'add_context_rule' as const
@@ -130,20 +122,6 @@ export function buildAddContextRuleArgs(
130
122
  'a rule with no signers and no policies is refused at install (NoSignersAndPolicies)'
131
123
  )
132
124
  }
133
- // OZ's spending_limit caps spend on the CONTEXT CONTRACT - it takes no
134
- // token argument - so it refuses a Default rule on chain with a bare
135
- // #3227. Say which rule shape it needs here, before the encoding, rather
136
- // than letting the user read a numeric trap.
137
- if (
138
- args.policies.some((p) => p.kind === 'oz_builtin' && p.primitive.primitive === 'spending_limit')
139
- ) {
140
- if (draft.contextRuleType.kind !== 'call_contract') {
141
- throw limitError(
142
- 'INSTALL_BUILD_FAILED',
143
- `spending_limit caps spend on the rule's context contract, so the rule must be call_contract-scoped to that token - a "${draft.contextRuleType.kind}" rule is refused on chain (#3227)`
144
- )
145
- }
146
- }
147
125
 
148
126
  // ---- 2. context_type encoding ----
149
127
  const contextType = encodeContextRuleType(draft.contextRuleType)
@@ -173,15 +151,6 @@ const POLICY_INSTALL_PARAM_FIELDS = [
173
151
  'install_nonce',
174
152
  'predicate',
175
153
  'predicate_hash',
176
- 'oracle_max_staleness_seconds',
177
- 'oracle_max_deviation_bps',
178
- // Cross-feed divergence bound. A field added to the contract's
179
- // `PolicyInstallParams` changes the ABI: the host unpacks the map by field
180
- // count, so omitting one fails the entire install with
181
- // `Error(Object, UnexpectedSize)` and no indication of which field is
182
- // missing. The Rust tests build that struct in Rust and never cross this
183
- // boundary, so only a real install against a deployed contract catches it.
184
- 'oracle_max_xfeed_dev_bps',
185
154
  ] as const
186
155
 
187
156
  function encodeContextRuleType(
@@ -232,92 +201,12 @@ function encodePoliciesMap(args: BuildAddContextRuleArgs): xdr.ScVal {
232
201
  val: encodePolicyInstallParams(args),
233
202
  })
234
203
  )
235
- } else if (ref.kind === 'oz_builtin') {
236
- entries.push(
237
- new xdr.ScMapEntry({
238
- key: Address.fromString(ref.instanceAddress).toScVal(),
239
- val: encodeOzPrimitiveParams(ref.primitive),
240
- })
241
- )
242
204
  }
243
205
  }
244
206
  entries.sort(sortByScValSymbolString)
245
207
  return xdr.ScVal.scvMap(entries)
246
208
  }
247
209
 
248
- /** Encode the install params for one OZ built-in policy.
249
- *
250
- * Field names and types are read from the deployed contracts' own spec:
251
- * SpendingLimitAccountParams { period_ledgers: u32, spending_limit: i128 }
252
- * SimpleThresholdAccountParams { threshold: u32 }
253
- * WeightedThresholdAccountParams { signer_weights: map, threshold: u32 }
254
- * Entries are symbol-string ordered for the same reason the interpreter's
255
- * are: the host orders by symbol, not by XDR bytes.
256
- */
257
- function encodeOzPrimitiveParams(primitive: OZPrimitiveConfig): xdr.ScVal {
258
- const entries: xdr.ScMapEntry[] = []
259
- const push = (name: string, val: xdr.ScVal) =>
260
- entries.push(new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol(name), val }))
261
- const p = primitive.params as Record<string, unknown>
262
- switch (primitive.primitive) {
263
- case 'spending_limit': {
264
- const limit = p.spending_limit
265
- const period = p.period_ledgers
266
- if (typeof limit !== 'string' || typeof period !== 'number') {
267
- throw limitError(
268
- 'INSTALL_BUILD_FAILED',
269
- 'spending_limit needs { spending_limit: string; period_ledgers: number }'
270
- )
271
- }
272
- push('period_ledgers', xdr.ScVal.scvU32(period))
273
- push('spending_limit', encodeI128(limit))
274
- break
275
- }
276
- case 'simple_threshold': {
277
- const threshold = p.threshold
278
- if (typeof threshold !== 'number') {
279
- throw limitError('INSTALL_BUILD_FAILED', 'simple_threshold needs { threshold: number }')
280
- }
281
- push('threshold', xdr.ScVal.scvU32(threshold))
282
- break
283
- }
284
- case 'weighted_threshold': {
285
- const threshold = p.threshold
286
- const weights = p.weights as Record<string, number> | undefined
287
- if (typeof threshold !== 'number' || !weights) {
288
- throw limitError(
289
- 'INSTALL_BUILD_FAILED',
290
- 'weighted_threshold needs { threshold: number; weights: Record<address, number> }'
291
- )
292
- }
293
- const weightEntries = Object.entries(weights)
294
- .map(
295
- ([addr, w]) =>
296
- new xdr.ScMapEntry({
297
- key: Address.fromString(addr).toScVal(),
298
- val: xdr.ScVal.scvU32(w),
299
- })
300
- )
301
- .sort((a, b) => (a.key().toXDR('base64') < b.key().toXDR('base64') ? -1 : 1))
302
- push('signer_weights', xdr.ScVal.scvMap(weightEntries))
303
- push('threshold', xdr.ScVal.scvU32(threshold))
304
- break
305
- }
306
- }
307
- entries.sort((a, b) => sortBySymbolString(a.key(), b.key()))
308
- return xdr.ScVal.scvMap(entries)
309
- }
310
-
311
- function encodeI128(value: string): xdr.ScVal {
312
- const v = BigInt(value)
313
- return xdr.ScVal.scvI128(
314
- new xdr.Int128Parts({
315
- hi: new xdr.Int64(BigInt.asIntN(64, v >> 64n)),
316
- lo: new xdr.Uint64(BigInt.asUintN(64, v)),
317
- })
318
- )
319
- }
320
-
321
210
  function encodePolicyInstallParams(args: BuildAddContextRuleArgs): xdr.ScVal {
322
211
  const predicate = Buffer.from(args.encodedPredicate, 'base64')
323
212
  const computedHash = createHash('sha256').update(predicate).digest('hex')
@@ -330,15 +219,11 @@ function encodePolicyInstallParams(args: BuildAddContextRuleArgs): xdr.ScVal {
330
219
  // Per-field value encoders. Each entry returns the ScVal to drop into the
331
220
  // map; the key symbol comes from the surrounding loop. Adding a field is
332
221
  // one row here plus the symbol in POLICY_INSTALL_PARAM_FIELDS.
333
- const oracleParams = args.oracleParams
334
222
  const valueFor: Record<(typeof POLICY_INSTALL_PARAM_FIELDS)[number], () => xdr.ScVal> = {
335
223
  grammar_version: () => xdr.ScVal.scvU32(args.grammarVersion ?? DEFAULT_GRAMMAR_VERSION),
336
224
  install_nonce: () => xdr.ScVal.scvU32(args.installNonce),
337
225
  predicate: () => xdr.ScVal.scvBytes(predicate),
338
226
  predicate_hash: () => xdr.ScVal.scvBytes(Buffer.from(args.predicateHash, 'hex')),
339
- oracle_max_staleness_seconds: () => encodeOptionU32(oracleParams?.maxStalenessSeconds),
340
- oracle_max_deviation_bps: () => encodeOptionU32(oracleParams?.maxDeviationBps),
341
- oracle_max_xfeed_dev_bps: () => encodeOptionU32(oracleParams?.maxCrossFeedDeviationBps),
342
227
  }
343
228
  const entries = POLICY_INSTALL_PARAM_FIELDS.map(
344
229
  (k) => new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol(k), val: valueFor[k]() })
@@ -351,14 +236,6 @@ function encodePolicyInstallParams(args: BuildAddContextRuleArgs): xdr.ScVal {
351
236
  return xdr.ScVal.scvMap(entries)
352
237
  }
353
238
 
354
- function encodeOptionU32(v: number | undefined): xdr.ScVal {
355
- if (v === undefined) return xdr.ScVal.scvVoid()
356
- if (!Number.isFinite(v) || v < 0 || v > 0xffffffff) {
357
- throw limitError('INSTALL_BUILD_FAILED', `oracle params value ${v} is outside u32 range`)
358
- }
359
- return xdr.ScVal.scvU32(v)
360
- }
361
-
362
239
  /** Sort host-map keys by their symbol-string form. The host orders map
363
240
  * entries by the SYMBOL STRING, not by the key's XDR bytes (a length
364
241
  * prefix in the encoding would otherwise put `amount` before `address`).
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { createHash } from 'node:crypto'
21
21
  import {
22
+ Account,
22
23
  Address,
23
24
  BASE_FEE,
24
25
  Contract,
@@ -30,7 +31,8 @@ import {
30
31
  TransactionBuilder,
31
32
  xdr,
32
33
  } from '@stellar/stellar-sdk'
33
- import { buildAddContextRuleArgs, DEFAULT_GRAMMAR_VERSION } from './build-add-context-rule.ts'
34
+ import type { ContextRuleDraft } from '../types.ts'
35
+ import { buildAddContextRuleArgs } from './build-add-context-rule.ts'
34
36
  import {
35
37
  accountEntry,
36
38
  authDigest,
@@ -106,20 +108,14 @@ export interface BuildInstallPolicyArgs {
106
108
  sourceAccount: string
107
109
  /** Network passphrase - pins the hash the auth digest binds. */
108
110
  networkPassphrase: string
109
- /** The new rule to install. Mirrors the core `ContextRuleDraft`. */
110
- rule: BuildInstallPolicyRuleDraft
111
+ /** The new rule to install. */
112
+ rule: ContextRuleDraft
111
113
  /** Per-rule install nonce; 1 for a fresh install. */
112
114
  installNonce: number
113
115
  /** Already-encoded (base64) canonical ScVal of the predicate. */
114
116
  encodedPredicate: string
115
117
  /** Hex sha256 of the canonical predicate XDR bytes. */
116
118
  predicateHash: string
117
- /** Per-policy oracle overrides. */
118
- oracleParams?: {
119
- maxStalenessSeconds?: number
120
- maxDeviationBps?: number
121
- maxCrossFeedDeviationBps?: number
122
- }
123
119
  /** Grammar version override; defaults to 1. */
124
120
  grammarVersion?: number
125
121
  /** Base fee in stroops; defaults to BASE_FEE (100). */
@@ -130,36 +126,6 @@ export interface BuildInstallPolicyArgs {
130
126
  authValidUntilLedgers?: number
131
127
  }
132
128
 
133
- export type BuildInstallPolicyRuleDraft = {
134
- contextRuleType:
135
- | { kind: 'default' }
136
- | { kind: 'call_contract'; contract: string }
137
- | { kind: 'create_contract'; wasmHash: string }
138
- name: string
139
- validUntilLedger: number | null
140
- signers: BuildInstallPolicySignerDraft[]
141
- policies: BuildInstallPolicyPolicyRef[]
142
- }
143
-
144
- export type BuildInstallPolicySignerDraft =
145
- | { kind: 'delegated'; address: string }
146
- | { kind: 'external'; verifier: string; keyBytes: string }
147
-
148
- export type BuildInstallPolicyPolicyRef =
149
- | {
150
- kind: 'interpreter'
151
- interpreterAddress: string
152
- predicateBlobBase64: string
153
- }
154
- | {
155
- kind: 'oz_builtin'
156
- instanceAddress: string
157
- primitive: {
158
- primitive: 'spending_limit' | 'simple_threshold' | 'weighted_threshold'
159
- params: Record<string, unknown>
160
- }
161
- }
162
-
163
129
  /** Human-readable description of the install call, decoded FROM the built
164
130
  * XDR (not from the input args). The XDR is the source of truth: a
165
131
  * human approving a review card needs the description to mirror the bytes
@@ -193,20 +159,13 @@ export interface InstallCallDescribes {
193
159
  * sha256 of the predicate blob actually embedded in the XDR - so a
194
160
  * mismatch between the wire bytes and the review card is detectable
195
161
  * by reading `describes`. */
196
- policies: Array<
197
- | {
198
- kind: 'interpreter'
199
- address: string
200
- installNonce: number
201
- predicateHash: string
202
- predicateSha256OfEmbeddedBytes: string
203
- }
204
- | {
205
- kind: 'oz_builtin'
206
- address: string
207
- primitive: 'spending_limit' | 'simple_threshold' | 'weighted_threshold'
208
- }
209
- >
162
+ policies: Array<{
163
+ kind: 'interpreter'
164
+ address: string
165
+ installNonce: number
166
+ predicateHash: string
167
+ predicateSha256OfEmbeddedBytes: string
168
+ }>
210
169
  /** The install nonce, decoded from the interpreter policy's
211
170
  * `install_nonce` field. Echoed at the top level for reviewer convenience;
212
171
  * the per-policy entry is the source of truth. */
@@ -261,102 +220,30 @@ export async function buildInstallPolicyXdr(
261
220
  name: args.rule.name,
262
221
  validUntilLedger: args.rule.validUntilLedger,
263
222
  signers: args.rule.signers,
264
- policies: args.rule.policies.map(adaptPolicyRef),
223
+ policies: args.rule.policies,
265
224
  },
266
225
  {
267
226
  signers: args.rule.signers,
268
- policies: args.rule.policies.map(adaptPolicyRef),
227
+ policies: args.rule.policies,
269
228
  installNonce: args.installNonce,
270
229
  encodedPredicate: args.encodedPredicate,
271
230
  predicateHash: args.predicateHash,
272
- ...(args.oracleParams ? { oracleParams: args.oracleParams } : {}),
273
231
  ...(args.grammarVersion !== undefined ? { grammarVersion: args.grammarVersion } : {}),
274
232
  }
275
233
  )
276
234
 
277
- // 2. Fetch the source sequence, then build the recording transaction with a
278
- // bare host call. The recording pass assigns the smart-account auth nonce
279
- // and root invocation needed to construct OZ's AuthPayload.
280
- const source = await args.rpc.getAccount(args.sourceAccount)
281
- const hostFunction = xdr.HostFunction.hostFunctionTypeInvokeContract(
282
- new xdr.InvokeContractArgs({
283
- contractAddress: new Address(args.smartAccount).toScAddress(),
284
- functionName: 'add_context_rule',
285
- args: [...callArgs],
286
- })
287
- )
288
- const makeOperation = (auth: xdr.SorobanAuthorizationEntry[] = []) =>
289
- Operation.invokeHostFunction({ func: hostFunction, auth })
290
- const baseFee = args.baseFee !== undefined ? String(args.baseFee) : BASE_FEE
291
- const buildTx = (op: xdr.Operation) =>
292
- buildUnsignedTx({
293
- sourceAccount: args.sourceAccount,
294
- sequence: source.sequenceNumber(),
295
- fee: baseFee,
296
- networkPassphrase: args.networkPassphrase,
297
- op,
298
- })
299
- const recordingTx = buildTx(makeOperation())
300
-
301
- // 3. Recording simulation: capture the smart account's nonce and invocation
302
- // tree. Nothing is signed here.
303
- const recorded = await args.rpc.simulateTransaction(recordingTx)
304
- if (rpc.Api.isSimulationError(recorded)) {
305
- // Short, stable reason. The full `simulateTransaction` error (which
306
- // carries host + URL detail) stays in the SDK's own logs - never
307
- // reflected back into a user-facing message where it would
308
- // reconnoitre the RPC.
309
- throw new Error('install_policy: simulateTransaction failed')
310
- }
311
- const original = (recorded.result?.auth ?? []).find(
312
- (entry) =>
313
- entry.credentials().switch().name === 'sorobanCredentialsAddress' &&
314
- Address.fromScAddress(entry.credentials().address().address()).toString() ===
315
- args.smartAccount
316
- )
317
- if (!original) {
318
- throw new Error(
319
- `install_policy: no Soroban auth entry for smart account ${args.smartAccount}; this call does not route through the smart account`
320
- )
321
- }
322
-
323
- // 4. `add_context_rule` is authorised by the deploy-time admin rule (rule 0).
235
+ // 2. `add_context_rule` is authorised by the deploy-time admin rule (rule 0).
324
236
  // The delegated signer uses source-account credentials, so its signature
325
237
  // bytes stay empty and the ordinary transaction-envelope signature covers
326
238
  // the call when the consumer signs it.
327
- const validUntilLedger =
328
- (await args.rpc.getLatestLedger()).sequence +
329
- (args.authValidUntilLedgers ?? DEFAULT_AUTH_VALID_UNTIL_LEDGERS)
330
- const contextRuleIds = [0]
331
- const digest = authDigest(
332
- signaturePayload(
333
- args.networkPassphrase,
334
- original.credentials().address().nonce(),
335
- validUntilLedger,
336
- original.rootInvocation()
337
- ),
338
- contextRuleIds
239
+ const { finalTx, original, validUntilLedger } = await buildAuthorisedSmartAccountTx(
240
+ args,
241
+ 'add_context_rule',
242
+ [...callArgs],
243
+ 'install_policy'
339
244
  )
340
- const authEntries = [
341
- accountEntry(
342
- original,
343
- validUntilLedger,
344
- authPayload([args.sourceAccount], contextRuleIds, () => Buffer.alloc(0))
345
- ),
346
- ...contextRuleIds.map(() => delegatedSignerEntry(args.smartAccount, digest)),
347
- ]
348
245
 
349
- // 5. Simulate again with the OZ entries already attached. The SDK preserves
350
- // existing auth during assembly and adds the simulated Soroban footprint +
351
- // resource fee, yielding a complete unsigned envelope.
352
- const txWithAuth = buildTx(makeOperation(authEntries))
353
- const enforcing = await args.rpc.simulateTransaction(txWithAuth)
354
- if (rpc.Api.isSimulationError(enforcing)) {
355
- throw new Error('install_policy: auth simulateTransaction failed')
356
- }
357
- const finalTx = rpc.assembleTransaction(txWithAuth, enforcing).build()
358
-
359
- // 6. Decode the structured description FROM the final assembled transaction.
246
+ // 3. Decode the structured description FROM the final assembled transaction.
360
247
  // The human approval binds to the exact bytes the wallet will sign.
361
248
  const describes = decodeInstallCallDescribes(finalTx, args.installNonce)
362
249
 
@@ -392,12 +279,75 @@ export async function buildRevokePolicyXdr(args: {
392
279
  baseFee?: number
393
280
  authValidUntilLedgers?: number
394
281
  }): Promise<BuildRevokePolicyResult> {
282
+ // Removal is master-only, so the deploy-time admin rule authorises it. The
283
+ // recorded rootInvocation still includes remove_context_rule(ruleId), binding
284
+ // the auth payload to the exact rule being removed. As with install,
285
+ // source-account credentials carry the delegated signer entry and the
286
+ // consumer supplies only the ordinary envelope signature.
287
+ const { finalTx, original, validUntilLedger } = await buildAuthorisedSmartAccountTx(
288
+ args,
289
+ 'remove_context_rule',
290
+ [xdr.ScVal.scvU32(args.ruleId)],
291
+ 'revoke_policy'
292
+ )
293
+
294
+ return {
295
+ unsignedXdr: finalTx.toEnvelope().toXDR().toString('base64'),
296
+ smartAccount: args.smartAccount,
297
+ sourceAccount: args.sourceAccount,
298
+ call: { contract: args.smartAccount, fn: 'remove_context_rule', ruleId: args.ruleId },
299
+ authNonce: original.credentials().address().nonce().toString(),
300
+ authValidUntilLedger: validUntilLedger,
301
+ rootInvocationXdr: original.rootInvocation().toXDR().toString('base64'),
302
+ }
303
+ }
304
+
305
+ export interface BuildRevokePolicyResult {
306
+ unsignedXdr: string
307
+ smartAccount: string
308
+ sourceAccount: string
309
+ call: { contract: string; fn: 'remove_context_rule'; ruleId: number }
310
+ authNonce: string
311
+ authValidUntilLedger: number
312
+ rootInvocationXdr: string
313
+ }
314
+
315
+ /** ~25 minutes at 5s/ledger. */
316
+ const DEFAULT_AUTH_VALID_UNTIL_LEDGERS = 300
317
+
318
+ // ---- internals ----
319
+
320
+ /** Record a bare call to the smart account, attach the deploy-time admin rule's
321
+ * auth entries, and re-simulate to assemble the footprint.
322
+ *
323
+ * Install and revoke differ only in the call they wrap, so they share this
324
+ * spine: the recording pass supplies the auth nonce and root invocation OZ's
325
+ * AuthPayload binds to, and the second simulation adds the Soroban footprint +
326
+ * resource fee. Nothing here is signed. `errorPrefix` names the calling tool in
327
+ * the fail-closed messages so a caller can still tell which call failed. */
328
+ async function buildAuthorisedSmartAccountTx(
329
+ args: {
330
+ smartAccount: string
331
+ sourceAccount: string
332
+ networkPassphrase: string
333
+ rpc: InstallRpcClient
334
+ baseFee?: number
335
+ authValidUntilLedgers?: number
336
+ },
337
+ functionName: string,
338
+ callArgs: xdr.ScVal[],
339
+ errorPrefix: string
340
+ ): Promise<{
341
+ finalTx: Transaction
342
+ original: xdr.SorobanAuthorizationEntry
343
+ validUntilLedger: number
344
+ }> {
395
345
  const source = await args.rpc.getAccount(args.sourceAccount)
396
346
  const hostFunction = xdr.HostFunction.hostFunctionTypeInvokeContract(
397
347
  new xdr.InvokeContractArgs({
398
348
  contractAddress: new Address(args.smartAccount).toScAddress(),
399
- functionName: 'remove_context_rule',
400
- args: [xdr.ScVal.scvU32(args.ruleId)],
349
+ functionName,
350
+ args: callArgs,
401
351
  })
402
352
  )
403
353
  const makeOperation = (auth: xdr.SorobanAuthorizationEntry[] = []) =>
@@ -418,7 +368,7 @@ export async function buildRevokePolicyXdr(args: {
418
368
  // carries host + URL detail) stays in the SDK's own logs - never
419
369
  // reflected back into a user-facing message where it would
420
370
  // reconnoitre the RPC.
421
- throw new Error('revoke_policy: simulateTransaction failed')
371
+ throw new Error(`${errorPrefix}: simulateTransaction failed`)
422
372
  }
423
373
  const original = (recorded.result?.auth ?? []).find(
424
374
  (entry) =>
@@ -428,15 +378,10 @@ export async function buildRevokePolicyXdr(args: {
428
378
  )
429
379
  if (!original) {
430
380
  throw new Error(
431
- `revoke_policy: no Soroban auth entry for smart account ${args.smartAccount}; this call does not route through the smart account`
381
+ `${errorPrefix}: no Soroban auth entry for smart account ${args.smartAccount}; this call does not route through the smart account`
432
382
  )
433
383
  }
434
384
 
435
- // Removal is master-only, so the deploy-time admin rule authorises it. The
436
- // recorded rootInvocation still includes remove_context_rule(ruleId), binding
437
- // the auth payload to the exact rule being removed. As with install,
438
- // source-account credentials carry the delegated signer entry and the
439
- // consumer supplies only the ordinary envelope signature.
440
385
  const validUntilLedger =
441
386
  (await args.rpc.getLatestLedger()).sequence +
442
387
  (args.authValidUntilLedgers ?? DEFAULT_AUTH_VALID_UNTIL_LEDGERS)
@@ -461,52 +406,12 @@ export async function buildRevokePolicyXdr(args: {
461
406
  const txWithAuth = buildTx(makeOperation(authEntries))
462
407
  const enforcing = await args.rpc.simulateTransaction(txWithAuth)
463
408
  if (rpc.Api.isSimulationError(enforcing)) {
464
- throw new Error('revoke_policy: auth simulateTransaction failed')
465
- }
466
- const finalTx = rpc.assembleTransaction(txWithAuth, enforcing).build()
467
-
468
- return {
469
- unsignedXdr: finalTx.toEnvelope().toXDR().toString('base64'),
470
- smartAccount: args.smartAccount,
471
- sourceAccount: args.sourceAccount,
472
- call: { contract: args.smartAccount, fn: 'remove_context_rule', ruleId: args.ruleId },
473
- authNonce: original.credentials().address().nonce().toString(),
474
- authValidUntilLedger: validUntilLedger,
475
- rootInvocationXdr: original.rootInvocation().toXDR().toString('base64'),
476
- }
477
- }
478
-
479
- export interface BuildRevokePolicyResult {
480
- unsignedXdr: string
481
- smartAccount: string
482
- sourceAccount: string
483
- call: { contract: string; fn: 'remove_context_rule'; ruleId: number }
484
- authNonce: string
485
- authValidUntilLedger: number
486
- rootInvocationXdr: string
487
- }
488
-
489
- /** ~25 minutes at 5s/ledger. */
490
- const DEFAULT_AUTH_VALID_UNTIL_LEDGERS = 300
491
-
492
- // ---- internals ----
493
-
494
- /** Adapter: turn the install-policy wire `PolicyRef` shape into the core
495
- * `PolicyRef` shape `buildAddContextRuleArgs` expects. Keeps the wire
496
- * schema hand-rolled + flat (the strict union would need a recursive
497
- * schema) while delegating to the proven encoder for the actual bytes. */
498
- function adaptPolicyRef(p: BuildInstallPolicyPolicyRef) {
499
- if (p.kind === 'interpreter') {
500
- return {
501
- kind: 'interpreter' as const,
502
- interpreterAddress: p.interpreterAddress,
503
- predicateBlobBase64: p.predicateBlobBase64,
504
- }
409
+ throw new Error(`${errorPrefix}: auth simulateTransaction failed`)
505
410
  }
506
411
  return {
507
- kind: 'oz_builtin' as const,
508
- instanceAddress: p.instanceAddress,
509
- primitive: p.primitive,
412
+ finalTx: rpc.assembleTransaction(txWithAuth, enforcing).build(),
413
+ original,
414
+ validUntilLedger,
510
415
  }
511
416
  }
512
417
 
@@ -580,14 +485,13 @@ function decodeInstallCallDescribes(
580
485
  `install_policy: built op has ${scvArgs.length} args, expected 5 (context_type, name, valid_until, signers, policies)`
581
486
  )
582
487
  }
583
- const contextType = scvArgs[0]
584
488
  const nameScv = scvArgs[1]
585
489
  const validUntilScv = scvArgs[2]
586
490
  const signersScv = scvArgs[3]
587
491
  const policiesScv = scvArgs[4]
588
492
  // The length check above pins these as defined; the local references
589
493
  // satisfy noUncheckedIndexedAccess on the accessors below.
590
- if (!nameScv || !validUntilScv || !signersScv || !policiesScv || !contextType) {
494
+ if (!nameScv || !validUntilScv || !signersScv || !policiesScv) {
591
495
  throw new Error('install_policy: built op args include an undefined slot despite length check')
592
496
  }
593
497
 
@@ -672,7 +576,6 @@ function decodeInstallCallDescribes(
672
576
  fields.set(fk.sym().toString(), inner.val())
673
577
  }
674
578
  // Interpreter policy fields carry grammar_version/install_nonce/predicate.
675
- // OZ primitives carry spending_limit/period_ledgers/threshold/signers.
676
579
  if (fields.has('predicate') || fields.has('grammar_version') || fields.has('install_nonce')) {
677
580
  const installNonceScv = fields.get('install_nonce')
678
581
  if (installNonceScv?.switch().name !== 'scvU32') {
@@ -706,21 +609,6 @@ function decodeInstallCallDescribes(
706
609
  observedInstallNonce = installNonce
707
610
  continue
708
611
  }
709
- // OZ built-in primitive. Distinguish by the parameter shape we
710
- // emitted; matching one of the three primitives exactly pins the
711
- // kind we built.
712
- if (fields.has('spending_limit') && fields.has('period_ledgers')) {
713
- policies.push({ kind: 'oz_builtin', address, primitive: 'spending_limit' })
714
- continue
715
- }
716
- if (fields.has('threshold') && fields.has('signer_weights')) {
717
- policies.push({ kind: 'oz_builtin', address, primitive: 'weighted_threshold' })
718
- continue
719
- }
720
- if (fields.has('threshold')) {
721
- policies.push({ kind: 'oz_builtin', address, primitive: 'simple_threshold' })
722
- continue
723
- }
724
612
  throw new Error(
725
613
  `install_policy: policies[${address}] value has an unknown field set; the encoder may have drifted`
726
614
  )
@@ -752,12 +640,3 @@ function decodeInstallCallDescribes(
752
640
  installNonce,
753
641
  }
754
642
  }
755
-
756
- /** Re-export so the run-layer does not need to import from
757
- * build-add-context-rule.ts (keeps the install/ -> install/ dependency
758
- * direction intact). */
759
- export { Contract, DEFAULT_GRAMMAR_VERSION }
760
-
761
- // Local re-import to avoid pulling the class from the SDK module path
762
- // at the top of the file (avoids the unused-import lint).
763
- import { Account } from '@stellar/stellar-sdk'