@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,308 @@
1
+ "use strict";
2
+ // src/install/build-add-context-rule.ts - pure builder for the OZ
3
+ // `add_context_rule` invocation arguments.
4
+ //
5
+ // Hand-rolled instead of reusing the scripts' helpers because the product
6
+ // path (Phase 06 install_policy) needs a single callable that takes a typed
7
+ // `ContextRuleDraft` + predicate bytes and returns the `ScVal` argument list
8
+ // the host will pass to `add_context_rule`. The script-level helpers stay
9
+ // out of `src/` to avoid dragging the ts-node SDK into the library surface.
10
+ //
11
+ // The OZ trait signature is pinned in
12
+ // `stellar-contracts::smart_account::SmartAccount::add_context_rule`:
13
+ //
14
+ // fn add_context_rule(
15
+ // e: &Env,
16
+ // context_type: ContextRuleType,
17
+ // name: String,
18
+ // valid_until: Option<u32>,
19
+ // signers: Vec<Signer>,
20
+ // policies: Map<Address, Val>,
21
+ // ) -> ContextRule
22
+ //
23
+ // The default body calls `e.current_contract_address().require_auth()`, so
24
+ // installation goes through the account's `__check_auth` -> `do_check_auth`.
25
+ // That requires the DEPLOYER to have shipped an admin rule with NO
26
+ // restrictive policy, otherwise the policy would refuse the very call that
27
+ // installs it. The caller is responsible for that contract; this builder
28
+ // just emits the args.
29
+ //
30
+ // Two hard limits the contract enforces (verified against
31
+ // `storage::validate_signers_and_policies` at /tmp/ozsc):
32
+ // - signers.len() <= MAX_SIGNERS (15)
33
+ // - policies.len() <= MAX_POLICIES (5)
34
+ // - signers and policies must not both be empty
35
+ // The same limits are mirrored in `OZ_LIMITS` (see src/types.ts) and
36
+ // checked here fail-closed before any encoding work - the user gets a
37
+ // typed error with the specific cap, not a runtime trap.
38
+ //
39
+ // Policypayloads are `Map<Address, Val>`. The host orders map entries by the
40
+ // symbol STRING of the key, NOT by the key's XDR bytes (a length prefix
41
+ // would otherwise put `amount` before `address` and produce a map the
42
+ // contract reads differently - a previous session lost hours to this). Our
43
+ // only key is an `Address`, so we build a single-entry map and the ordering
44
+ // question is moot, but the comment is here so the next map-bearing code
45
+ // in this file does not regress.
46
+ //
47
+ // Pure: no network, no signing. The caller hands the returned ScVal[] to
48
+ // `Operation.invokeHostFunction` AFTER wiring the auth entries, otherwise
49
+ // the account refuses with `Error(Auth, InvalidAction)`.
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.ADD_CONTEXT_RULE_SYMBOL = exports.DEFAULT_GRAMMAR_VERSION = void 0;
52
+ exports.buildAddContextRuleArgs = buildAddContextRuleArgs;
53
+ const node_crypto_1 = require("node:crypto");
54
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
55
+ const types_ts_1 = require("../types.js");
56
+ exports.DEFAULT_GRAMMAR_VERSION = 1;
57
+ /** The verb `add_context_rule` takes on the wire. */
58
+ exports.ADD_CONTEXT_RULE_SYMBOL = 'add_context_rule';
59
+ /** Build the `add_context_rule` invocation args. Throws a `ToolError`-shaped
60
+ * error on limit breaches or malformed input. */
61
+ function buildAddContextRuleArgs(draft, args) {
62
+ // ---- 1. Cap checks (fail-closed before any encoding) ----
63
+ if (args.signers.length > types_ts_1.OZ_LIMITS.maxSignersPerRule) {
64
+ throw limitError('INSTALL_BUILD_FAILED', `signers ${args.signers.length} exceed MAX_SIGNERS_PER_RULE ${types_ts_1.OZ_LIMITS.maxSignersPerRule}`);
65
+ }
66
+ if (args.policies.length > types_ts_1.OZ_LIMITS.maxPoliciesPerRule) {
67
+ throw limitError('INSTALL_BUILD_FAILED', `policies ${args.policies.length} exceed MAX_POLICIES_PER_RULE ${types_ts_1.OZ_LIMITS.maxPoliciesPerRule}`);
68
+ }
69
+ if (args.signers.length === 0 && args.policies.length === 0) {
70
+ throw limitError('INSTALL_BUILD_FAILED', 'a rule with no signers and no policies is refused at install (NoSignersAndPolicies)');
71
+ }
72
+ // OZ's spending_limit caps spend on the CONTEXT CONTRACT - it takes no
73
+ // token argument - so it refuses a Default rule on chain with a bare
74
+ // #3227. Say which rule shape it needs here, before the encoding, rather
75
+ // than letting the user read a numeric trap.
76
+ if (args.policies.some((p) => p.kind === 'oz_builtin' && p.primitive.primitive === 'spending_limit')) {
77
+ if (draft.contextRuleType.kind !== 'call_contract') {
78
+ throw limitError('INSTALL_BUILD_FAILED', `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)`);
79
+ }
80
+ }
81
+ // ---- 2. context_type encoding ----
82
+ const contextType = encodeContextRuleType(draft.contextRuleType);
83
+ // ---- 3. name (string) + valid_until (Option<u32>) ----
84
+ const name = stellar_sdk_1.xdr.ScVal.scvString(draft.name);
85
+ const validUntil = draft.validUntilLedger === null ? stellar_sdk_1.xdr.ScVal.scvVoid() : stellar_sdk_1.xdr.ScVal.scvU32(draft.validUntilLedger);
86
+ // ---- 4. signers ----
87
+ const signersVec = stellar_sdk_1.xdr.ScVal.scvVec(args.signers.map(encodeSigner));
88
+ // ---- 5. policies Map<Address, Val> ----
89
+ const policies = encodePoliciesMap(args);
90
+ return [contextType, name, validUntil, signersVec, policies];
91
+ }
92
+ // ---- helpers ----
93
+ /** Field order of the `PolicyInstallParams` struct. The contract's
94
+ * `#[contracttype]` derives a Map<Symbol, Val> encoding, so the host
95
+ * sorts by symbol STRING (NOT by XDR bytes). This explicit list is the
96
+ * single source of truth for the field order. */
97
+ const POLICY_INSTALL_PARAM_FIELDS = [
98
+ 'grammar_version',
99
+ 'install_nonce',
100
+ 'predicate',
101
+ 'predicate_hash',
102
+ 'oracle_max_staleness_seconds',
103
+ 'oracle_max_deviation_bps',
104
+ // Cross-feed divergence bound. A field added to the contract's
105
+ // `PolicyInstallParams` changes the ABI: the host unpacks the map by field
106
+ // count, so omitting one fails the entire install with
107
+ // `Error(Object, UnexpectedSize)` and no indication of which field is
108
+ // missing. The Rust tests build that struct in Rust and never cross this
109
+ // boundary, so only a real install against a deployed contract catches it.
110
+ 'oracle_max_xfeed_dev_bps',
111
+ ];
112
+ function encodeContextRuleType(rule) {
113
+ switch (rule.kind) {
114
+ case 'default':
115
+ return stellar_sdk_1.xdr.ScVal.scvVec([stellar_sdk_1.xdr.ScVal.scvSymbol('Default')]);
116
+ case 'call_contract':
117
+ return stellar_sdk_1.xdr.ScVal.scvVec([
118
+ stellar_sdk_1.xdr.ScVal.scvSymbol('CallContract'),
119
+ stellar_sdk_1.Address.fromString(rule.contract).toScVal(),
120
+ ]);
121
+ case 'create_contract':
122
+ return stellar_sdk_1.xdr.ScVal.scvVec([
123
+ stellar_sdk_1.xdr.ScVal.scvSymbol('CreateContract'),
124
+ stellar_sdk_1.xdr.ScVal.scvBytes(Buffer.from(rule.wasmHash, 'hex')),
125
+ ]);
126
+ }
127
+ }
128
+ function encodeSigner(s) {
129
+ switch (s.kind) {
130
+ case 'delegated':
131
+ return stellar_sdk_1.xdr.ScVal.scvVec([
132
+ stellar_sdk_1.xdr.ScVal.scvSymbol('Delegated'),
133
+ stellar_sdk_1.Address.fromString(s.address).toScVal(),
134
+ ]);
135
+ case 'external':
136
+ return stellar_sdk_1.xdr.ScVal.scvVec([
137
+ stellar_sdk_1.xdr.ScVal.scvSymbol('External'),
138
+ stellar_sdk_1.Address.fromString(s.verifier).toScVal(),
139
+ stellar_sdk_1.xdr.ScVal.scvBytes(Buffer.from(s.keyBytes, 'hex')),
140
+ ]);
141
+ }
142
+ }
143
+ function encodePoliciesMap(args) {
144
+ const entries = [];
145
+ for (const ref of args.policies) {
146
+ if (ref.kind === 'interpreter') {
147
+ const val = encodePolicyInstallParams(args);
148
+ entries.push(new stellar_sdk_1.xdr.ScMapEntry({
149
+ key: stellar_sdk_1.Address.fromString(ref.interpreterAddress).toScVal(),
150
+ val,
151
+ }));
152
+ }
153
+ if (ref.kind === 'oz_builtin') {
154
+ entries.push(new stellar_sdk_1.xdr.ScMapEntry({
155
+ key: stellar_sdk_1.Address.fromString(ref.instanceAddress).toScVal(),
156
+ val: encodeOzPrimitiveParams(ref.primitive),
157
+ }));
158
+ }
159
+ }
160
+ entries.sort(sortByScValSymbolString);
161
+ return stellar_sdk_1.xdr.ScVal.scvMap(entries);
162
+ }
163
+ /** Encode the install params for one OZ built-in policy.
164
+ *
165
+ * Field names and types are read from the deployed contracts' own spec:
166
+ * SpendingLimitAccountParams { period_ledgers: u32, spending_limit: i128 }
167
+ * SimpleThresholdAccountParams { threshold: u32 }
168
+ * WeightedThresholdAccountParams { signer_weights: map, threshold: u32 }
169
+ * Entries are symbol-string ordered for the same reason the interpreter's
170
+ * are: the host orders by symbol, not by XDR bytes.
171
+ */
172
+ function encodeOzPrimitiveParams(primitive) {
173
+ const entries = [];
174
+ const push = (name, val) => entries.push(new stellar_sdk_1.xdr.ScMapEntry({ key: stellar_sdk_1.xdr.ScVal.scvSymbol(name), val }));
175
+ const p = primitive.params;
176
+ switch (primitive.primitive) {
177
+ case 'spending_limit': {
178
+ const limit = p.spending_limit;
179
+ const period = p.period_ledgers;
180
+ if (typeof limit !== 'string' || typeof period !== 'number') {
181
+ throw limitError('INSTALL_BUILD_FAILED', 'spending_limit needs { spending_limit: string; period_ledgers: number }');
182
+ }
183
+ push('period_ledgers', stellar_sdk_1.xdr.ScVal.scvU32(period));
184
+ push('spending_limit', encodeI128(limit));
185
+ break;
186
+ }
187
+ case 'simple_threshold': {
188
+ const threshold = p.threshold;
189
+ if (typeof threshold !== 'number') {
190
+ throw limitError('INSTALL_BUILD_FAILED', 'simple_threshold needs { threshold: number }');
191
+ }
192
+ push('threshold', stellar_sdk_1.xdr.ScVal.scvU32(threshold));
193
+ break;
194
+ }
195
+ case 'weighted_threshold': {
196
+ const threshold = p.threshold;
197
+ const weights = p.weights;
198
+ if (typeof threshold !== 'number' || !weights) {
199
+ throw limitError('INSTALL_BUILD_FAILED', 'weighted_threshold needs { threshold: number; weights: Record<address, number> }');
200
+ }
201
+ const weightEntries = Object.entries(weights)
202
+ .map(([addr, w]) => new stellar_sdk_1.xdr.ScMapEntry({
203
+ key: stellar_sdk_1.Address.fromString(addr).toScVal(),
204
+ val: stellar_sdk_1.xdr.ScVal.scvU32(w),
205
+ }))
206
+ .sort((a, b) => (a.key().toXDR('base64') < b.key().toXDR('base64') ? -1 : 1));
207
+ push('signer_weights', stellar_sdk_1.xdr.ScVal.scvMap(weightEntries));
208
+ push('threshold', stellar_sdk_1.xdr.ScVal.scvU32(threshold));
209
+ break;
210
+ }
211
+ }
212
+ entries.sort((a, b) => sortBySymbolString(a.key(), b.key()));
213
+ return stellar_sdk_1.xdr.ScVal.scvMap(entries);
214
+ }
215
+ function encodeI128(value) {
216
+ const v = BigInt(value);
217
+ return stellar_sdk_1.xdr.ScVal.scvI128(new stellar_sdk_1.xdr.Int128Parts({
218
+ hi: new stellar_sdk_1.xdr.Int64(BigInt.asIntN(64, v >> 64n)),
219
+ lo: new stellar_sdk_1.xdr.Uint64(BigInt.asUintN(64, v)),
220
+ }));
221
+ }
222
+ function encodePolicyInstallParams(args) {
223
+ const predicate = Buffer.from(args.encodedPredicate, 'base64');
224
+ const computedHash = (0, node_crypto_1.createHash)('sha256').update(predicate).digest('hex');
225
+ if (computedHash !== args.predicateHash) {
226
+ throw limitError('INSTALL_BUILD_FAILED', `predicateHash ${args.predicateHash.slice(0, 16)}... does not match sha256(encodedPredicate) ${computedHash.slice(0, 16)}...`);
227
+ }
228
+ const entries = [];
229
+ for (const k of POLICY_INSTALL_PARAM_FIELDS) {
230
+ const key = stellar_sdk_1.xdr.ScVal.scvSymbol(k);
231
+ switch (k) {
232
+ case 'grammar_version':
233
+ entries.push(new stellar_sdk_1.xdr.ScMapEntry({
234
+ key,
235
+ val: stellar_sdk_1.xdr.ScVal.scvU32(args.grammarVersion ?? exports.DEFAULT_GRAMMAR_VERSION),
236
+ }));
237
+ break;
238
+ case 'install_nonce':
239
+ entries.push(new stellar_sdk_1.xdr.ScMapEntry({ key, val: stellar_sdk_1.xdr.ScVal.scvU32(args.installNonce) }));
240
+ break;
241
+ case 'predicate':
242
+ entries.push(new stellar_sdk_1.xdr.ScMapEntry({ key, val: stellar_sdk_1.xdr.ScVal.scvBytes(predicate) }));
243
+ break;
244
+ case 'predicate_hash':
245
+ entries.push(new stellar_sdk_1.xdr.ScMapEntry({
246
+ key,
247
+ val: stellar_sdk_1.xdr.ScVal.scvBytes(Buffer.from(args.predicateHash, 'hex')),
248
+ }));
249
+ break;
250
+ case 'oracle_max_staleness_seconds':
251
+ entries.push(new stellar_sdk_1.xdr.ScMapEntry({
252
+ key,
253
+ val: encodeOptionU32(args.oracleParams?.maxStalenessSeconds),
254
+ }));
255
+ break;
256
+ case 'oracle_max_deviation_bps':
257
+ entries.push(new stellar_sdk_1.xdr.ScMapEntry({
258
+ key,
259
+ val: encodeOptionU32(args.oracleParams?.maxDeviationBps),
260
+ }));
261
+ break;
262
+ case 'oracle_max_xfeed_dev_bps':
263
+ entries.push(new stellar_sdk_1.xdr.ScMapEntry({
264
+ key,
265
+ val: encodeOptionU32(args.oracleParams?.maxCrossFeedDeviationBps),
266
+ }));
267
+ break;
268
+ }
269
+ }
270
+ // The host orders map entries by the SYMBOL STRING, not by XDR bytes. A
271
+ // length prefix in the XDR encoding would otherwise put `amount` before
272
+ // `address` and produce a struct the contract reads differently. Emit the
273
+ // entries in symbol-string order so the wire form matches the host's view.
274
+ entries.sort((a, b) => sortBySymbolString(a.key(), b.key()));
275
+ return stellar_sdk_1.xdr.ScVal.scvMap(entries);
276
+ }
277
+ function encodeOptionU32(v) {
278
+ if (v === undefined)
279
+ return stellar_sdk_1.xdr.ScVal.scvVoid();
280
+ if (!Number.isFinite(v) || v < 0 || v > 0xffffffff) {
281
+ throw limitError('INSTALL_BUILD_FAILED', `oracle params value ${v} is outside u32 range`);
282
+ }
283
+ return stellar_sdk_1.xdr.ScVal.scvU32(v);
284
+ }
285
+ /** Sort host-map keys by their symbol-string form. The host orders map
286
+ * entries by the SYMBOL STRING, not by the key's XDR bytes (a length
287
+ * prefix in the encoding would otherwise put `amount` before `address`).
288
+ * For non-symbol keys we fall back to length-prefixed XDR bytes; the
289
+ * caller's only Address keys today are length-stable so this is moot, but
290
+ * the helper stands ready for the multi-policy variant. */
291
+ function sortByScValSymbolString(a, b) {
292
+ return sortBySymbolString(a.key(), b.key());
293
+ }
294
+ function sortBySymbolString(a, b) {
295
+ const aSym = a.switch().name === 'scvSymbol' ? a.sym().toString() : null;
296
+ const bSym = b.switch().name === 'scvSymbol' ? b.sym().toString() : null;
297
+ if (aSym !== null && bSym !== null) {
298
+ return aSym < bSym ? -1 : aSym > bSym ? 1 : 0;
299
+ }
300
+ return Buffer.compare(a.toXDR(), b.toXDR());
301
+ }
302
+ function limitError(code, message) {
303
+ const err = new Error(message);
304
+ err.code = code;
305
+ err.severity = 'error';
306
+ err.retryable = false;
307
+ throw err;
308
+ }
@@ -53,6 +53,10 @@ export interface IRCompare {
53
53
  selector: IRSelector;
54
54
  operator: IRCompOp;
55
55
  value: string;
56
+ /** Decimal basis of `value`, for selectors whose comparand is not on a basis
57
+ * the contract can infer. Set for `oracle_price`, where prices normalise to
58
+ * 9 dp and an undeclared basis fails OPEN. */
59
+ valueDecimals?: number;
56
60
  }
57
61
  /** Condition tree. NEAR-V2 guard/constraint are flat And/Or; the IR allows
58
62
  * nesting + `not` + `in` + `eq_seq` so the same IR can later lower to the OZ
@@ -81,6 +85,25 @@ export type IRCondition = {
81
85
  op: 'eq_seq';
82
86
  selector: IRSelector;
83
87
  values: string[];
88
+ }
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;
84
107
  };
85
108
  export interface IRPolicyRule {
86
109
  /** NEAR-V2 roles whitelist (empty = any; owner exempt). */
@@ -0,0 +1,11 @@
1
+ import { xdr } from '@stellar/stellar-sdk';
2
+ import type { PredicateLeaf, PredicateNode } from '../types.ts';
3
+ /** One leaf. Order of the bare-literal checks mirrors `decode_leaf` in dsl.rs. */
4
+ export declare function decodeLeaf(v: xdr.ScVal): PredicateLeaf;
5
+ /** One node. `not` wraps a NODE; the comparison ops wrap two LEAVES. */
6
+ export declare function decodeNode(v: xdr.ScVal): PredicateNode;
7
+ /** Decode a predicate from the wire bytes the interpreter stores.
8
+ *
9
+ * Accepts the base64 the encoder emits (`encodePredicate().encodedPredicate`)
10
+ * or the raw XDR bytes read straight out of `StoredDoc.predicate_bytes`. */
11
+ export declare function decodePredicate(encoded: string | Uint8Array): PredicateNode;
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ // src/predicate/decode.ts - the inverse of `encode.ts`.
3
+ //
4
+ // Turns the canonical wire encoding back into a `PredicateNode`, so a policy
5
+ // already installed on chain can be read and explained. The interpreter has no
6
+ // getter for the document, but its persistent storage IS readable over RPC, so
7
+ // the bytes are available to any caller that can reconstruct the storage key -
8
+ // this module takes it from there.
9
+ //
10
+ // Mirrors `decode_leaf` / `decode_node` in
11
+ // `packages/policy-interpreter/src/dsl.rs`. Two rules carry most of the
12
+ // weight, and both come from the Rust decoder verbatim:
13
+ //
14
+ // 1. A `Vec` whose FIRST element is a Symbol is a selector tuple. A `Vec`
15
+ // whose first element is anything else is a `literal_vec`, decoded
16
+ // element-wise with order preserved (the order IS the semantic for exact
17
+ // sequence equality).
18
+ // 2. An unknown symbol at a selector position is MALFORMED, never a
19
+ // literal_vec fallback. Guessing there would let an unsupported selector
20
+ // decode as data and read as permitted.
21
+ //
22
+ // The round-trip `decode(encode(node)) === node` is pinned in decode.test.ts
23
+ // for every leaf and node shape; that test is what keeps this file honest as
24
+ // the grammar grows.
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.decodeLeaf = decodeLeaf;
27
+ exports.decodeNode = decodeNode;
28
+ exports.decodePredicate = decodePredicate;
29
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
30
+ /** Same shape the encoder raises, so both directions surface one error type.
31
+ * Returns `never` - it always throws; callers `throw malformed(...)` purely
32
+ * so the control flow reads at the call site. */
33
+ function malformed(detail) {
34
+ const err = new Error(`cannot decode predicate: ${detail}`);
35
+ err.code = 'MALFORMED_PREDICATE';
36
+ err.severity = 'error';
37
+ err.retryable = false;
38
+ throw err;
39
+ }
40
+ /** The symbol at a leaf's selector position, or null when the value is not a
41
+ * symbol-headed vec (i.e. it is a literal or a literal vector). */
42
+ function selectorSymbol(items) {
43
+ const head = items[0];
44
+ if (!head)
45
+ return null;
46
+ return head.switch() === stellar_sdk_1.xdr.ScValType.scvSymbol() ? head.sym().toString() : null;
47
+ }
48
+ function expectVec(v, what) {
49
+ if (v.switch() !== stellar_sdk_1.xdr.ScValType.scvVec())
50
+ throw malformed(`${what} is not a vec`);
51
+ return v.vec() ?? [];
52
+ }
53
+ function expectU32(v, what) {
54
+ if (!v || v.switch() !== stellar_sdk_1.xdr.ScValType.scvU32())
55
+ throw malformed(`${what} is not a u32`);
56
+ return v.u32();
57
+ }
58
+ function expectSymbol(v, what) {
59
+ if (!v || v.switch() !== stellar_sdk_1.xdr.ScValType.scvSymbol())
60
+ throw malformed(`${what} is not a symbol`);
61
+ return v.sym().toString();
62
+ }
63
+ function expectAddress(v, what) {
64
+ if (!v || v.switch() !== stellar_sdk_1.xdr.ScValType.scvAddress())
65
+ throw malformed(`${what} is not an address`);
66
+ return stellar_sdk_1.Address.fromScAddress(v.address()).toString();
67
+ }
68
+ function expectU64(v, what) {
69
+ if (!v || v.switch() !== stellar_sdk_1.xdr.ScValType.scvU64())
70
+ throw malformed(`${what} is not a u64`);
71
+ return v.u64().toString();
72
+ }
73
+ function expectI128(v, what) {
74
+ if (!v || v.switch() !== stellar_sdk_1.xdr.ScValType.scvI128())
75
+ throw malformed(`${what} is not an i128`);
76
+ return (0, stellar_sdk_1.scValToBigInt)(v).toString();
77
+ }
78
+ /** Arity check with the same intent as the Rust `check_arity`: a selector with
79
+ * the wrong element count is malformed, not silently truncated. */
80
+ function arity(items, n, selector) {
81
+ if (items.length !== n) {
82
+ throw malformed(`${selector} expects ${n} elements, got ${items.length}`);
83
+ }
84
+ }
85
+ function decodeSelectorLeaf(items, sym) {
86
+ switch (sym) {
87
+ case 'call_contract':
88
+ arity(items, 1, sym);
89
+ return { kind: 'call_contract' };
90
+ case 'call_fn':
91
+ arity(items, 1, sym);
92
+ return { kind: 'call_fn' };
93
+ case 'call_arg':
94
+ arity(items, 2, sym);
95
+ return { kind: 'call_arg', index: expectU32(items[1], 'call_arg index') };
96
+ case 'call_arg_len':
97
+ arity(items, 2, sym);
98
+ return { kind: 'call_arg_len', index: expectU32(items[1], 'call_arg_len index') };
99
+ case 'call_arg_field':
100
+ arity(items, 4, sym);
101
+ return {
102
+ kind: 'call_arg_field',
103
+ index: expectU32(items[1], 'call_arg_field index'),
104
+ element: expectU32(items[2], 'call_arg_field element'),
105
+ field: expectSymbol(items[3], 'call_arg_field field'),
106
+ };
107
+ case 'call_arg_scaled':
108
+ arity(items, 4, sym);
109
+ return {
110
+ kind: 'call_arg_scaled',
111
+ index: expectU32(items[1], 'call_arg_scaled index'),
112
+ num: expectI128(items[2], 'call_arg_scaled num'),
113
+ den: expectI128(items[3], 'call_arg_scaled den'),
114
+ };
115
+ case 'amount':
116
+ arity(items, 2, sym);
117
+ return { kind: 'amount', token: expectAddress(items[1], 'amount token') };
118
+ case 'window_spent':
119
+ arity(items, 3, sym);
120
+ return {
121
+ kind: 'window_spent',
122
+ token: expectAddress(items[1], 'window_spent token'),
123
+ windowSeconds: Number(expectU64(items[2], 'window_spent windowSeconds')),
124
+ };
125
+ case 'now':
126
+ arity(items, 1, sym);
127
+ return { kind: 'now' };
128
+ case 'valid_until':
129
+ // The encoder refuses to emit this and the interpreter refuses it at
130
+ // install, so it cannot appear in a document that installed. Decoding it
131
+ // anyway would present a rule the chain would never have accepted.
132
+ throw malformed('valid_until is not a usable predicate leaf');
133
+ case 'invocation_count':
134
+ arity(items, 2, sym);
135
+ return {
136
+ kind: 'invocation_count_in_window',
137
+ windowSecs: Number(expectU64(items[1], 'invocation_count windowSecs')),
138
+ };
139
+ case 'oracle_price':
140
+ arity(items, 2, sym);
141
+ return { kind: 'oracle_price', asset: expectAddress(items[1], 'oracle_price asset') };
142
+ case 'oracle_threshold':
143
+ arity(items, 3, sym);
144
+ return {
145
+ kind: 'oracle_threshold',
146
+ value: expectI128(items[1], 'oracle_threshold value'),
147
+ decimals: expectU32(items[2], 'oracle_threshold decimals'),
148
+ };
149
+ default:
150
+ // Deliberately NOT a literal_vec fallback - see the header note.
151
+ throw malformed(`unknown selector symbol '${sym}'`);
152
+ }
153
+ }
154
+ /** One leaf. Order of the bare-literal checks mirrors `decode_leaf` in dsl.rs. */
155
+ function decodeLeaf(v) {
156
+ if (v.switch() === stellar_sdk_1.xdr.ScValType.scvVec()) {
157
+ const items = v.vec() ?? [];
158
+ if (items.length === 0)
159
+ throw malformed('empty vec is neither a selector nor a literal vector');
160
+ const sym = selectorSymbol(items);
161
+ if (sym !== null)
162
+ return decodeSelectorLeaf(items, sym);
163
+ return { kind: 'literal_vec', elements: items.map(decodeLeaf) };
164
+ }
165
+ switch (v.switch()) {
166
+ case stellar_sdk_1.xdr.ScValType.scvAddress():
167
+ return { kind: 'literal_address', value: stellar_sdk_1.Address.fromScAddress(v.address()).toString() };
168
+ case stellar_sdk_1.xdr.ScValType.scvSymbol():
169
+ return { kind: 'literal_symbol', value: v.sym().toString() };
170
+ case stellar_sdk_1.xdr.ScValType.scvU32():
171
+ return { kind: 'literal_u32', value: v.u32() };
172
+ case stellar_sdk_1.xdr.ScValType.scvU64():
173
+ return { kind: 'literal_u64', value: v.u64().toString() };
174
+ case stellar_sdk_1.xdr.ScValType.scvI128():
175
+ return { kind: 'literal_i128', value: (0, stellar_sdk_1.scValToBigInt)(v).toString() };
176
+ case stellar_sdk_1.xdr.ScValType.scvBytes():
177
+ return { kind: 'literal_bytes', value: Buffer.from(v.bytes()).toString('hex') };
178
+ default:
179
+ throw malformed(`unsupported leaf value type ${v.switch().name}`);
180
+ }
181
+ }
182
+ /** One node. `not` wraps a NODE; the comparison ops wrap two LEAVES. */
183
+ function decodeNode(v) {
184
+ const items = expectVec(v, 'node');
185
+ const op = selectorSymbol(items);
186
+ if (op === null)
187
+ throw malformed('node does not start with an operator symbol');
188
+ switch (op) {
189
+ case 'and':
190
+ case 'or': {
191
+ arity(items, 2, op);
192
+ const children = expectVec(items[1], `${op} children`).map(decodeNode);
193
+ if (children.length === 0)
194
+ throw malformed(`${op} has no children`);
195
+ return { op, children };
196
+ }
197
+ case 'not':
198
+ arity(items, 2, op);
199
+ return { op: 'not', child: decodeNode(items[1]) };
200
+ case 'eq':
201
+ case 'lt':
202
+ case 'lte':
203
+ case 'gt':
204
+ case 'gte':
205
+ arity(items, 3, op);
206
+ return {
207
+ op,
208
+ left: decodeLeaf(items[1]),
209
+ right: decodeLeaf(items[2]),
210
+ };
211
+ case 'in': {
212
+ arity(items, 3, op);
213
+ return {
214
+ op: 'in',
215
+ needle: decodeLeaf(items[1]),
216
+ haystack: expectVec(items[2], 'in haystack').map(decodeLeaf),
217
+ };
218
+ }
219
+ default:
220
+ throw malformed(`unknown node operator '${op}'`);
221
+ }
222
+ }
223
+ /** Decode a predicate from the wire bytes the interpreter stores.
224
+ *
225
+ * Accepts the base64 the encoder emits (`encodePredicate().encodedPredicate`)
226
+ * or the raw XDR bytes read straight out of `StoredDoc.predicate_bytes`. */
227
+ function decodePredicate(encoded) {
228
+ let scv;
229
+ try {
230
+ scv =
231
+ typeof encoded === 'string'
232
+ ? stellar_sdk_1.xdr.ScVal.fromXDR(Buffer.from(encoded, 'base64'))
233
+ : stellar_sdk_1.xdr.ScVal.fromXDR(Buffer.from(encoded));
234
+ }
235
+ catch (e) {
236
+ throw malformed(`not valid ScVal XDR (${e instanceof Error ? e.message : String(e)})`);
237
+ }
238
+ return decodeNode(scv);
239
+ }