@crediolabs/policy-synth 0.1.10 → 0.1.12

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 (84) hide show
  1. package/dist/adapters/interpreter/adapter.js +83 -11
  2. package/dist/adapters/oz/adapter.js +4 -0
  3. package/dist/errors.d.ts +1 -1
  4. package/dist/install/build-add-context-rule.d.ts +48 -0
  5. package/dist/install/build-add-context-rule.js +304 -0
  6. package/dist/ir/types.d.ts +23 -0
  7. package/dist/predicate/decode.d.ts +11 -0
  8. package/dist/predicate/decode.js +234 -0
  9. package/dist/predicate/encode.js +79 -14
  10. package/dist/predicate/from-json.d.ts +4 -0
  11. package/dist/predicate/from-json.js +113 -0
  12. package/dist/predicate/index.d.ts +2 -0
  13. package/dist/predicate/index.js +5 -1
  14. package/dist/registry/identify.js +21 -2
  15. package/dist/registry/protocols.d.ts +50 -1
  16. package/dist/registry/protocols.js +88 -0
  17. package/dist/review-card/builder.d.ts +13 -0
  18. package/dist/review-card/builder.js +63 -5
  19. package/dist/review-card/cross-check.js +32 -3
  20. package/dist/review-card/index.d.ts +1 -1
  21. package/dist/review-card/index.js +1 -1
  22. package/dist/run/schemas.d.ts +4 -4
  23. package/dist/synth/compose-from-recording.d.ts +18 -0
  24. package/dist/synth/compose-from-recording.js +75 -10
  25. package/dist/synth/deny-cases.d.ts +4 -1
  26. package/dist/synth/deny-cases.js +3 -1
  27. package/dist/synth/evaluate.js +130 -3
  28. package/dist/synth/permit-context.d.ts +15 -0
  29. package/dist/synth/permit-context.js +116 -0
  30. package/dist/synth/synthesize-from-recording.js +14 -2
  31. package/dist/types.d.ts +9 -0
  32. package/dist-cjs/adapters/interpreter/adapter.js +83 -11
  33. package/dist-cjs/adapters/oz/adapter.js +4 -0
  34. package/dist-cjs/errors.d.ts +1 -1
  35. package/dist-cjs/install/build-add-context-rule.d.ts +48 -0
  36. package/dist-cjs/install/build-add-context-rule.js +308 -0
  37. package/dist-cjs/ir/types.d.ts +23 -0
  38. package/dist-cjs/predicate/decode.d.ts +11 -0
  39. package/dist-cjs/predicate/decode.js +239 -0
  40. package/dist-cjs/predicate/encode.js +79 -14
  41. package/dist-cjs/predicate/from-json.d.ts +4 -0
  42. package/dist-cjs/predicate/from-json.js +116 -0
  43. package/dist-cjs/predicate/index.d.ts +2 -0
  44. package/dist-cjs/predicate/index.js +10 -2
  45. package/dist-cjs/registry/identify.js +20 -1
  46. package/dist-cjs/registry/protocols.d.ts +50 -1
  47. package/dist-cjs/registry/protocols.js +89 -1
  48. package/dist-cjs/review-card/builder.d.ts +13 -0
  49. package/dist-cjs/review-card/builder.js +64 -5
  50. package/dist-cjs/review-card/cross-check.js +32 -3
  51. package/dist-cjs/review-card/index.d.ts +1 -1
  52. package/dist-cjs/review-card/index.js +2 -1
  53. package/dist-cjs/run/schemas.d.ts +4 -4
  54. package/dist-cjs/synth/compose-from-recording.d.ts +18 -0
  55. package/dist-cjs/synth/compose-from-recording.js +75 -10
  56. package/dist-cjs/synth/deny-cases.d.ts +4 -1
  57. package/dist-cjs/synth/deny-cases.js +3 -0
  58. package/dist-cjs/synth/evaluate.js +130 -3
  59. package/dist-cjs/synth/permit-context.d.ts +15 -0
  60. package/dist-cjs/synth/permit-context.js +119 -0
  61. package/dist-cjs/synth/synthesize-from-recording.js +14 -2
  62. package/dist-cjs/types.d.ts +9 -0
  63. package/package.json +5 -2
  64. package/src/adapters/interpreter/adapter.ts +93 -11
  65. package/src/adapters/oz/adapter.ts +4 -0
  66. package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +28 -3
  67. package/src/errors.ts +13 -0
  68. package/src/install/build-add-context-rule.ts +429 -0
  69. package/src/ir/types.ts +23 -0
  70. package/src/predicate/decode.ts +242 -0
  71. package/src/predicate/encode.ts +110 -13
  72. package/src/predicate/from-json.ts +124 -0
  73. package/src/predicate/index.ts +5 -1
  74. package/src/registry/identify.ts +22 -2
  75. package/src/registry/protocols.ts +90 -1
  76. package/src/review-card/builder.ts +58 -5
  77. package/src/review-card/cross-check.ts +36 -3
  78. package/src/review-card/index.ts +1 -0
  79. package/src/synth/compose-from-recording.ts +101 -7
  80. package/src/synth/deny-cases.ts +3 -1
  81. package/src/synth/evaluate.ts +130 -3
  82. package/src/synth/permit-context.ts +136 -0
  83. package/src/synth/synthesize-from-recording.ts +13 -2
  84. package/src/types.ts +12 -0
@@ -0,0 +1,429 @@
1
+ // src/install/build-add-context-rule.ts - pure builder for the OZ
2
+ // `add_context_rule` invocation arguments.
3
+ //
4
+ // Hand-rolled instead of reusing the scripts' helpers because the product
5
+ // path (Phase 06 install_policy) needs a single callable that takes a typed
6
+ // `ContextRuleDraft` + predicate bytes and returns the `ScVal` argument list
7
+ // the host will pass to `add_context_rule`. The script-level helpers stay
8
+ // out of `src/` to avoid dragging the ts-node SDK into the library surface.
9
+ //
10
+ // The OZ trait signature is pinned in
11
+ // `stellar-contracts::smart_account::SmartAccount::add_context_rule`:
12
+ //
13
+ // fn add_context_rule(
14
+ // e: &Env,
15
+ // context_type: ContextRuleType,
16
+ // name: String,
17
+ // valid_until: Option<u32>,
18
+ // signers: Vec<Signer>,
19
+ // policies: Map<Address, Val>,
20
+ // ) -> ContextRule
21
+ //
22
+ // The default body calls `e.current_contract_address().require_auth()`, so
23
+ // installation goes through the account's `__check_auth` -> `do_check_auth`.
24
+ // That requires the DEPLOYER to have shipped an admin rule with NO
25
+ // restrictive policy, otherwise the policy would refuse the very call that
26
+ // installs it. The caller is responsible for that contract; this builder
27
+ // just emits the args.
28
+ //
29
+ // Two hard limits the contract enforces (verified against
30
+ // `storage::validate_signers_and_policies` at /tmp/ozsc):
31
+ // - signers.len() <= MAX_SIGNERS (15)
32
+ // - policies.len() <= MAX_POLICIES (5)
33
+ // - signers and policies must not both be empty
34
+ // The same limits are mirrored in `OZ_LIMITS` (see src/types.ts) and
35
+ // checked here fail-closed before any encoding work - the user gets a
36
+ // typed error with the specific cap, not a runtime trap.
37
+ //
38
+ // Policypayloads are `Map<Address, Val>`. The host orders map entries by the
39
+ // symbol STRING of the key, NOT by the key's XDR bytes (a length prefix
40
+ // would otherwise put `amount` before `address` and produce a map the
41
+ // contract reads differently - a previous session lost hours to this). Our
42
+ // only key is an `Address`, so we build a single-entry map and the ordering
43
+ // question is moot, but the comment is here so the next map-bearing code
44
+ // in this file does not regress.
45
+ //
46
+ // Pure: no network, no signing. The caller hands the returned ScVal[] to
47
+ // `Operation.invokeHostFunction` AFTER wiring the auth entries, otherwise
48
+ // the account refuses with `Error(Auth, InvalidAction)`.
49
+
50
+ import { createHash } from 'node:crypto'
51
+ import { Address, xdr } from '@stellar/stellar-sdk'
52
+ import type { ToolError } from '../errors.ts'
53
+ import {
54
+ type ContextRuleDraft,
55
+ OZ_LIMITS,
56
+ type OZPrimitiveConfig,
57
+ type PolicyRef,
58
+ type SignerDraft,
59
+ } from '../types.ts'
60
+
61
+ /** Inputs for `buildAddContextRuleArgs`. The fields are the ones the contract
62
+ * validates at install; everything else is downstream. */
63
+ export interface BuildAddContextRuleArgs {
64
+ /** The auth-context rule signers to attach - `Vec<Signer>` wire form.
65
+ * These are the SIGNERS the new rule authorises, not the deployer's
66
+ * admin session. The deployer signs the install call via the admin rule
67
+ * already on the account, which is separate from this list. */
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. */
71
+ policies: PolicyRef[]
72
+ /** Per-policy install nonce; 1 for a fresh install. */
73
+ installNonce: number
74
+ /** Already-encoded (base64) canonical ScVal of the predicate. The builder
75
+ * decodes it to bytes for the `predicate` field and re-hashes to confirm
76
+ * the caller-supplied `predicateHash` matches. */
77
+ encodedPredicate: string
78
+ /** Hex sha256 of the canonical predicate XDR bytes. */
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
+ /** Hard pin to the interpreter's wasm grammar. Refusing here fails closed
89
+ * before the chain does; the contract will refuse a mismatch again. */
90
+ grammarVersion?: number
91
+ }
92
+
93
+ export const DEFAULT_GRAMMAR_VERSION = 1 as const
94
+
95
+ /** The verb `add_context_rule` takes on the wire. */
96
+ export const ADD_CONTEXT_RULE_SYMBOL = 'add_context_rule' as const
97
+
98
+ /** Tuple of `ScVal` arguments to pass to `Operation.invokeHostFunction` for
99
+ * `add_context_rule`. Order matches the OZ trait signature. */
100
+ export type AddContextRuleArgs = readonly [
101
+ contextType: xdr.ScVal,
102
+ name: xdr.ScVal,
103
+ validUntil: xdr.ScVal,
104
+ signers: xdr.ScVal,
105
+ policies: xdr.ScVal,
106
+ ]
107
+
108
+ /** Build the `add_context_rule` invocation args. Throws a `ToolError`-shaped
109
+ * error on limit breaches or malformed input. */
110
+ export function buildAddContextRuleArgs(
111
+ draft: ContextRuleDraft,
112
+ args: BuildAddContextRuleArgs
113
+ ): AddContextRuleArgs {
114
+ // ---- 1. Cap checks (fail-closed before any encoding) ----
115
+ if (args.signers.length > OZ_LIMITS.maxSignersPerRule) {
116
+ throw limitError(
117
+ 'INSTALL_BUILD_FAILED',
118
+ `signers ${args.signers.length} exceed MAX_SIGNERS_PER_RULE ${OZ_LIMITS.maxSignersPerRule}`
119
+ )
120
+ }
121
+ if (args.policies.length > OZ_LIMITS.maxPoliciesPerRule) {
122
+ throw limitError(
123
+ 'INSTALL_BUILD_FAILED',
124
+ `policies ${args.policies.length} exceed MAX_POLICIES_PER_RULE ${OZ_LIMITS.maxPoliciesPerRule}`
125
+ )
126
+ }
127
+ if (args.signers.length === 0 && args.policies.length === 0) {
128
+ throw limitError(
129
+ 'INSTALL_BUILD_FAILED',
130
+ 'a rule with no signers and no policies is refused at install (NoSignersAndPolicies)'
131
+ )
132
+ }
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
+
148
+ // ---- 2. context_type encoding ----
149
+ const contextType = encodeContextRuleType(draft.contextRuleType)
150
+
151
+ // ---- 3. name (string) + valid_until (Option<u32>) ----
152
+ const name = xdr.ScVal.scvString(draft.name)
153
+ const validUntil =
154
+ draft.validUntilLedger === null ? xdr.ScVal.scvVoid() : xdr.ScVal.scvU32(draft.validUntilLedger)
155
+
156
+ // ---- 4. signers ----
157
+ const signersVec = xdr.ScVal.scvVec(args.signers.map(encodeSigner))
158
+
159
+ // ---- 5. policies Map<Address, Val> ----
160
+ const policies = encodePoliciesMap(args)
161
+
162
+ return [contextType, name, validUntil, signersVec, policies] as const
163
+ }
164
+
165
+ // ---- helpers ----
166
+
167
+ /** Field order of the `PolicyInstallParams` struct. The contract's
168
+ * `#[contracttype]` derives a Map<Symbol, Val> encoding, so the host
169
+ * sorts by symbol STRING (NOT by XDR bytes). This explicit list is the
170
+ * single source of truth for the field order. */
171
+ const POLICY_INSTALL_PARAM_FIELDS = [
172
+ 'grammar_version',
173
+ 'install_nonce',
174
+ 'predicate',
175
+ '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
+ ] as const
186
+
187
+ function encodeContextRuleType(
188
+ rule:
189
+ | { kind: 'default' }
190
+ | { kind: 'call_contract'; contract: string }
191
+ | { kind: 'create_contract'; wasmHash: string }
192
+ ): xdr.ScVal {
193
+ switch (rule.kind) {
194
+ case 'default':
195
+ return xdr.ScVal.scvVec([xdr.ScVal.scvSymbol('Default')])
196
+ case 'call_contract':
197
+ return xdr.ScVal.scvVec([
198
+ xdr.ScVal.scvSymbol('CallContract'),
199
+ Address.fromString(rule.contract).toScVal(),
200
+ ])
201
+ case 'create_contract':
202
+ return xdr.ScVal.scvVec([
203
+ xdr.ScVal.scvSymbol('CreateContract'),
204
+ xdr.ScVal.scvBytes(Buffer.from(rule.wasmHash, 'hex')),
205
+ ])
206
+ }
207
+ }
208
+
209
+ function encodeSigner(s: SignerDraft): xdr.ScVal {
210
+ switch (s.kind) {
211
+ case 'delegated':
212
+ return xdr.ScVal.scvVec([
213
+ xdr.ScVal.scvSymbol('Delegated'),
214
+ Address.fromString(s.address).toScVal(),
215
+ ])
216
+ case 'external':
217
+ return xdr.ScVal.scvVec([
218
+ xdr.ScVal.scvSymbol('External'),
219
+ Address.fromString(s.verifier).toScVal(),
220
+ xdr.ScVal.scvBytes(Buffer.from(s.keyBytes, 'hex')),
221
+ ])
222
+ }
223
+ }
224
+
225
+ function encodePoliciesMap(args: BuildAddContextRuleArgs): xdr.ScVal {
226
+ const entries: xdr.ScMapEntry[] = []
227
+ for (const ref of args.policies) {
228
+ if (ref.kind === 'interpreter') {
229
+ const val = encodePolicyInstallParams(args)
230
+ entries.push(
231
+ new xdr.ScMapEntry({
232
+ key: Address.fromString(ref.interpreterAddress).toScVal(),
233
+ val,
234
+ })
235
+ )
236
+ }
237
+ if (ref.kind === 'oz_builtin') {
238
+ entries.push(
239
+ new xdr.ScMapEntry({
240
+ key: Address.fromString(ref.instanceAddress).toScVal(),
241
+ val: encodeOzPrimitiveParams(ref.primitive),
242
+ })
243
+ )
244
+ }
245
+ }
246
+ entries.sort(sortByScValSymbolString)
247
+ return xdr.ScVal.scvMap(entries)
248
+ }
249
+
250
+ /** Encode the install params for one OZ built-in policy.
251
+ *
252
+ * Field names and types are read from the deployed contracts' own spec:
253
+ * SpendingLimitAccountParams { period_ledgers: u32, spending_limit: i128 }
254
+ * SimpleThresholdAccountParams { threshold: u32 }
255
+ * WeightedThresholdAccountParams { signer_weights: map, threshold: u32 }
256
+ * Entries are symbol-string ordered for the same reason the interpreter's
257
+ * are: the host orders by symbol, not by XDR bytes.
258
+ */
259
+ function encodeOzPrimitiveParams(primitive: OZPrimitiveConfig): xdr.ScVal {
260
+ const entries: xdr.ScMapEntry[] = []
261
+ const push = (name: string, val: xdr.ScVal) =>
262
+ entries.push(new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol(name), val }))
263
+ const p = primitive.params as Record<string, unknown>
264
+ switch (primitive.primitive) {
265
+ case 'spending_limit': {
266
+ const limit = p.spending_limit
267
+ const period = p.period_ledgers
268
+ if (typeof limit !== 'string' || typeof period !== 'number') {
269
+ throw limitError(
270
+ 'INSTALL_BUILD_FAILED',
271
+ 'spending_limit needs { spending_limit: string; period_ledgers: number }'
272
+ )
273
+ }
274
+ push('period_ledgers', xdr.ScVal.scvU32(period))
275
+ push('spending_limit', encodeI128(limit))
276
+ break
277
+ }
278
+ case 'simple_threshold': {
279
+ const threshold = p.threshold
280
+ if (typeof threshold !== 'number') {
281
+ throw limitError('INSTALL_BUILD_FAILED', 'simple_threshold needs { threshold: number }')
282
+ }
283
+ push('threshold', xdr.ScVal.scvU32(threshold))
284
+ break
285
+ }
286
+ case 'weighted_threshold': {
287
+ const threshold = p.threshold
288
+ const weights = p.weights as Record<string, number> | undefined
289
+ if (typeof threshold !== 'number' || !weights) {
290
+ throw limitError(
291
+ 'INSTALL_BUILD_FAILED',
292
+ 'weighted_threshold needs { threshold: number; weights: Record<address, number> }'
293
+ )
294
+ }
295
+ const weightEntries = Object.entries(weights)
296
+ .map(
297
+ ([addr, w]) =>
298
+ new xdr.ScMapEntry({
299
+ key: Address.fromString(addr).toScVal(),
300
+ val: xdr.ScVal.scvU32(w),
301
+ })
302
+ )
303
+ .sort((a, b) => (a.key().toXDR('base64') < b.key().toXDR('base64') ? -1 : 1))
304
+ push('signer_weights', xdr.ScVal.scvMap(weightEntries))
305
+ push('threshold', xdr.ScVal.scvU32(threshold))
306
+ break
307
+ }
308
+ }
309
+ entries.sort((a, b) => sortBySymbolString(a.key(), b.key()))
310
+ return xdr.ScVal.scvMap(entries)
311
+ }
312
+
313
+ function encodeI128(value: string): xdr.ScVal {
314
+ const v = BigInt(value)
315
+ return xdr.ScVal.scvI128(
316
+ new xdr.Int128Parts({
317
+ hi: new xdr.Int64(BigInt.asIntN(64, v >> 64n)),
318
+ lo: new xdr.Uint64(BigInt.asUintN(64, v)),
319
+ })
320
+ )
321
+ }
322
+
323
+ function encodePolicyInstallParams(args: BuildAddContextRuleArgs): xdr.ScVal {
324
+ const predicate = Buffer.from(args.encodedPredicate, 'base64')
325
+ const computedHash = createHash('sha256').update(predicate).digest('hex')
326
+ if (computedHash !== args.predicateHash) {
327
+ throw limitError(
328
+ 'INSTALL_BUILD_FAILED',
329
+ `predicateHash ${args.predicateHash.slice(0, 16)}... does not match sha256(encodedPredicate) ${computedHash.slice(0, 16)}...`
330
+ )
331
+ }
332
+ const entries: xdr.ScMapEntry[] = []
333
+ for (const k of POLICY_INSTALL_PARAM_FIELDS) {
334
+ const key = xdr.ScVal.scvSymbol(k)
335
+ switch (k) {
336
+ case 'grammar_version':
337
+ entries.push(
338
+ new xdr.ScMapEntry({
339
+ key,
340
+ val: xdr.ScVal.scvU32(args.grammarVersion ?? DEFAULT_GRAMMAR_VERSION),
341
+ })
342
+ )
343
+ break
344
+ case 'install_nonce':
345
+ entries.push(new xdr.ScMapEntry({ key, val: xdr.ScVal.scvU32(args.installNonce) }))
346
+ break
347
+ case 'predicate':
348
+ entries.push(new xdr.ScMapEntry({ key, val: xdr.ScVal.scvBytes(predicate) }))
349
+ break
350
+ case 'predicate_hash':
351
+ entries.push(
352
+ new xdr.ScMapEntry({
353
+ key,
354
+ val: xdr.ScVal.scvBytes(Buffer.from(args.predicateHash, 'hex')),
355
+ })
356
+ )
357
+ break
358
+ case 'oracle_max_staleness_seconds':
359
+ entries.push(
360
+ new xdr.ScMapEntry({
361
+ key,
362
+ val: encodeOptionU32(args.oracleParams?.maxStalenessSeconds),
363
+ })
364
+ )
365
+ break
366
+ case 'oracle_max_deviation_bps':
367
+ entries.push(
368
+ new xdr.ScMapEntry({
369
+ key,
370
+ val: encodeOptionU32(args.oracleParams?.maxDeviationBps),
371
+ })
372
+ )
373
+ break
374
+ case 'oracle_max_xfeed_dev_bps':
375
+ entries.push(
376
+ new xdr.ScMapEntry({
377
+ key,
378
+ val: encodeOptionU32(args.oracleParams?.maxCrossFeedDeviationBps),
379
+ })
380
+ )
381
+ break
382
+ }
383
+ }
384
+ // The host orders map entries by the SYMBOL STRING, not by XDR bytes. A
385
+ // length prefix in the XDR encoding would otherwise put `amount` before
386
+ // `address` and produce a struct the contract reads differently. Emit the
387
+ // entries in symbol-string order so the wire form matches the host's view.
388
+ entries.sort((a, b) => sortBySymbolString(a.key(), b.key()))
389
+ return xdr.ScVal.scvMap(entries)
390
+ }
391
+
392
+ function encodeOptionU32(v: number | undefined): xdr.ScVal {
393
+ if (v === undefined) return xdr.ScVal.scvVoid()
394
+ if (!Number.isFinite(v) || v < 0 || v > 0xffffffff) {
395
+ throw limitError('INSTALL_BUILD_FAILED', `oracle params value ${v} is outside u32 range`)
396
+ }
397
+ return xdr.ScVal.scvU32(v)
398
+ }
399
+
400
+ /** Sort host-map keys by their symbol-string form. The host orders map
401
+ * entries by the SYMBOL STRING, not by the key's XDR bytes (a length
402
+ * prefix in the encoding would otherwise put `amount` before `address`).
403
+ * For non-symbol keys we fall back to length-prefixed XDR bytes; the
404
+ * caller's only Address keys today are length-stable so this is moot, but
405
+ * the helper stands ready for the multi-policy variant. */
406
+ function sortByScValSymbolString(a: xdr.ScMapEntry, b: xdr.ScMapEntry): number {
407
+ return sortBySymbolString(a.key(), b.key())
408
+ }
409
+
410
+ function sortBySymbolString(a: xdr.ScVal, b: xdr.ScVal): number {
411
+ const aSym = a.switch().name === 'scvSymbol' ? a.sym().toString() : null
412
+ const bSym = b.switch().name === 'scvSymbol' ? b.sym().toString() : null
413
+ if (aSym !== null && bSym !== null) {
414
+ return aSym < bSym ? -1 : aSym > bSym ? 1 : 0
415
+ }
416
+ return Buffer.compare(a.toXDR(), b.toXDR())
417
+ }
418
+
419
+ function limitError(code: ToolError['code'], message: string): ToolError {
420
+ const err = new Error(message) as Error & {
421
+ code: ToolError['code']
422
+ severity: string
423
+ retryable: boolean
424
+ }
425
+ err.code = code
426
+ err.severity = 'error'
427
+ err.retryable = false
428
+ throw err
429
+ }
package/src/ir/types.ts CHANGED
@@ -65,6 +65,10 @@ export interface IRCompare {
65
65
  selector: IRSelector
66
66
  operator: IRCompOp
67
67
  value: string
68
+ /** Decimal basis of `value`, for selectors whose comparand is not on a basis
69
+ * the contract can infer. Set for `oracle_price`, where prices normalise to
70
+ * 9 dp and an undeclared basis fails OPEN. */
71
+ valueDecimals?: number
68
72
  }
69
73
 
70
74
  /** Condition tree. NEAR-V2 guard/constraint are flat And/Or; the IR allows
@@ -82,6 +86,25 @@ export type IRCondition =
82
86
  * does. Adapters that cannot express an exact ordered vector (OZ built-ins)
83
87
  * flag this as `uncovered` rather than silently dropping it. */
84
88
  | { op: 'eq_seq'; selector: IRSelector; values: string[] }
89
+ /** A swap's output floor, expressed against its own input: the output arg
90
+ * must be at least `inArgIndex * num / den`.
91
+ *
92
+ * It is its own construct because `compare` bounds a selector against a
93
+ * CONSTANT, and a slippage floor has no constant to bound against - the
94
+ * acceptable output depends on the input of the same call. Encoding the
95
+ * recorded output as a constant would pin a policy to one trade size.
96
+ *
97
+ * `num/den` is the caller's minimum acceptable output per unit of input.
98
+ * It is never derived from the recording: the recorded rate is a price at
99
+ * one moment, and freezing it as policy would deny normal trades later.
100
+ * Adapters that cannot express it (the OZ built-ins) flag it `uncovered`. */
101
+ | {
102
+ op: 'slippage_floor'
103
+ outArgIndex: number
104
+ inArgIndex: number
105
+ num: string
106
+ den: string
107
+ }
85
108
 
86
109
  export interface IRPolicyRule {
87
110
  /** NEAR-V2 roles whitelist (empty = any; owner exempt). */