@crediolabs/policy-synth 0.1.3 → 0.1.5
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.
- package/LICENSE +21 -0
- package/README.md +193 -5
- package/dist/adapters/interpreter/adapter.d.ts +38 -0
- package/dist/adapters/interpreter/adapter.js +522 -0
- package/dist/adapters/interpreter/index.d.ts +1 -0
- package/dist/adapters/interpreter/index.js +2 -0
- package/dist/adapters/oz/adapter.js +2 -0
- package/dist/codegen/compile-gate.d.ts +33 -0
- package/dist/codegen/compile-gate.js +119 -0
- package/dist/codegen/index.d.ts +2 -0
- package/dist/codegen/index.js +8 -0
- package/dist/codegen/template.d.ts +18 -0
- package/dist/codegen/template.js +131 -0
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/ir/types.d.ts +13 -2
- package/dist/predicate/encode.d.ts +10 -0
- package/dist/predicate/encode.js +249 -0
- package/dist/predicate/index.d.ts +1 -0
- package/dist/predicate/index.js +2 -0
- package/dist/record/decode.d.ts +6 -3
- package/dist/record/decode.js +13 -3
- package/dist/review-card/builder.d.ts +14 -0
- package/dist/review-card/builder.js +261 -0
- package/dist/review-card/conflict.d.ts +40 -0
- package/dist/review-card/conflict.js +111 -0
- package/dist/review-card/cross-check.d.ts +11 -0
- package/dist/review-card/cross-check.js +148 -0
- package/dist/review-card/index.d.ts +3 -0
- package/dist/review-card/index.js +4 -0
- package/dist/run/index.d.ts +31 -0
- package/dist/run/index.js +164 -0
- package/dist/run/schemas.d.ts +2003 -0
- package/dist/run/schemas.js +215 -0
- package/dist/synth/compose-from-recording.d.ts +35 -7
- package/dist/synth/compose-from-recording.js +225 -34
- package/dist/synth/deny-cases.d.ts +12 -0
- package/dist/synth/deny-cases.js +377 -0
- package/dist/synth/evaluate.d.ts +39 -0
- package/dist/synth/evaluate.js +425 -0
- package/dist/synth/harness.d.ts +16 -0
- package/dist/synth/harness.js +26 -0
- package/dist/synth/index.d.ts +4 -0
- package/dist/synth/index.js +4 -0
- package/dist/synth/minimize.d.ts +4 -0
- package/dist/synth/minimize.js +38 -0
- package/dist/synth/predicate-literals.d.ts +5 -0
- package/dist/synth/predicate-literals.js +25 -0
- package/dist/synth/synthesize-from-recording.d.ts +37 -5
- package/dist/synth/synthesize-from-recording.js +550 -18
- package/dist/types.d.ts +40 -1
- package/dist/types.js +17 -0
- package/dist/verify/envelope.d.ts +15 -0
- package/dist/verify/envelope.js +22 -0
- package/dist/verify/index.d.ts +3 -0
- package/dist/verify/index.js +3 -0
- package/dist/verify/simulate.d.ts +31 -0
- package/dist/verify/simulate.js +258 -0
- package/dist/verify/verify.d.ts +21 -0
- package/dist/verify/verify.js +189 -0
- package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
- package/dist-cjs/adapters/interpreter/adapter.js +527 -0
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
- package/dist-cjs/adapters/interpreter/index.js +8 -0
- package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
- package/dist-cjs/adapters/oz/adapter.js +289 -0
- package/dist-cjs/adapters/oz/index.d.ts +1 -0
- package/dist-cjs/adapters/oz/index.js +8 -0
- package/dist-cjs/codegen/compile-gate.d.ts +33 -0
- package/dist-cjs/codegen/compile-gate.js +123 -0
- package/dist-cjs/codegen/index.d.ts +2 -0
- package/dist-cjs/codegen/index.js +14 -0
- package/dist-cjs/codegen/template.d.ts +18 -0
- package/dist-cjs/codegen/template.js +134 -0
- package/dist-cjs/errors.d.ts +37 -0
- package/dist-cjs/errors.js +3 -0
- package/dist-cjs/index.d.ts +11 -0
- package/dist-cjs/index.js +27 -0
- package/dist-cjs/ir/index.d.ts +1 -0
- package/dist-cjs/ir/index.js +3 -0
- package/dist-cjs/ir/types.d.ts +108 -0
- package/dist-cjs/ir/types.js +12 -0
- package/dist-cjs/mandate/index.d.ts +2 -0
- package/dist-cjs/mandate/index.js +6 -0
- package/dist-cjs/mandate/to-ir.d.ts +3 -0
- package/dist-cjs/mandate/to-ir.js +63 -0
- package/dist-cjs/mandate/types.d.ts +20 -0
- package/dist-cjs/mandate/types.js +9 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/predicate/encode.d.ts +10 -0
- package/dist-cjs/predicate/encode.js +252 -0
- package/dist-cjs/predicate/index.d.ts +1 -0
- package/dist-cjs/predicate/index.js +6 -0
- package/dist-cjs/record/decode.d.ts +79 -0
- package/dist-cjs/record/decode.js +398 -0
- package/dist-cjs/record/freshness.d.ts +17 -0
- package/dist-cjs/record/freshness.js +55 -0
- package/dist-cjs/record/index.d.ts +21 -0
- package/dist-cjs/record/index.js +166 -0
- package/dist-cjs/record/movements.d.ts +20 -0
- package/dist-cjs/record/movements.js +192 -0
- package/dist-cjs/record/rpc.d.ts +22 -0
- package/dist-cjs/record/rpc.js +74 -0
- package/dist-cjs/record/validate.d.ts +22 -0
- package/dist-cjs/record/validate.js +63 -0
- package/dist-cjs/registry/identify.d.ts +11 -0
- package/dist-cjs/registry/identify.js +87 -0
- package/dist-cjs/registry/index.d.ts +3 -0
- package/dist-cjs/registry/index.js +15 -0
- package/dist-cjs/registry/known-addresses.d.ts +16 -0
- package/dist-cjs/registry/known-addresses.js +53 -0
- package/dist-cjs/registry/protocols.d.ts +38 -0
- package/dist-cjs/registry/protocols.js +153 -0
- package/dist-cjs/review-card/builder.d.ts +14 -0
- package/dist-cjs/review-card/builder.js +264 -0
- package/dist-cjs/review-card/conflict.d.ts +40 -0
- package/dist-cjs/review-card/conflict.js +114 -0
- package/dist-cjs/review-card/cross-check.d.ts +11 -0
- package/dist-cjs/review-card/cross-check.js +151 -0
- package/dist-cjs/review-card/index.d.ts +3 -0
- package/dist-cjs/review-card/index.js +10 -0
- package/dist-cjs/run/index.d.ts +31 -0
- package/dist-cjs/run/index.js +178 -0
- package/dist-cjs/run/schemas.d.ts +2003 -0
- package/dist-cjs/run/schemas.js +218 -0
- package/dist-cjs/seams/index.d.ts +1 -0
- package/dist-cjs/seams/index.js +3 -0
- package/dist-cjs/seams/types.d.ts +66 -0
- package/dist-cjs/seams/types.js +12 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +64 -0
- package/dist-cjs/synth/compose-from-recording.js +356 -0
- package/dist-cjs/synth/deny-cases.d.ts +12 -0
- package/dist-cjs/synth/deny-cases.js +380 -0
- package/dist-cjs/synth/evaluate.d.ts +39 -0
- package/dist-cjs/synth/evaluate.js +428 -0
- package/dist-cjs/synth/harness.d.ts +16 -0
- package/dist-cjs/synth/harness.js +29 -0
- package/dist-cjs/synth/index.d.ts +9 -0
- package/dist-cjs/synth/index.js +23 -0
- package/dist-cjs/synth/lower.d.ts +23 -0
- package/dist-cjs/synth/lower.js +119 -0
- package/dist-cjs/synth/minimize.d.ts +4 -0
- package/dist-cjs/synth/minimize.js +41 -0
- package/dist-cjs/synth/predicate-literals.d.ts +5 -0
- package/dist-cjs/synth/predicate-literals.js +28 -0
- package/dist-cjs/synth/scope.d.ts +26 -0
- package/dist-cjs/synth/scope.js +81 -0
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
- package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
- package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
- package/dist-cjs/synth/synthesize-from-recording.js +711 -0
- package/dist-cjs/types.d.ts +288 -0
- package/dist-cjs/types.js +55 -0
- package/dist-cjs/verify/envelope.d.ts +15 -0
- package/dist-cjs/verify/envelope.js +23 -0
- package/dist-cjs/verify/index.d.ts +3 -0
- package/dist-cjs/verify/index.js +8 -0
- package/dist-cjs/verify/simulate.d.ts +31 -0
- package/dist-cjs/verify/simulate.js +261 -0
- package/dist-cjs/verify/verify.d.ts +21 -0
- package/dist-cjs/verify/verify.js +192 -0
- package/package.json +43 -3
- package/src/adapters/interpreter/adapter.ts +642 -0
- package/src/adapters/interpreter/index.ts +8 -0
- package/src/adapters/oz/adapter.ts +2 -0
- package/src/codegen/compile-gate.ts +162 -0
- package/src/codegen/index.ts +17 -0
- package/src/codegen/template.ts +148 -0
- package/src/errors.ts +2 -0
- package/src/index.ts +2 -0
- package/src/ir/types.ts +9 -2
- package/src/predicate/encode.ts +307 -0
- package/src/predicate/index.ts +3 -0
- package/src/record/decode.ts +21 -10
- package/src/review-card/builder.ts +303 -0
- package/src/review-card/conflict.ts +143 -0
- package/src/review-card/cross-check.ts +158 -0
- package/src/review-card/index.ts +12 -0
- package/src/run/index.ts +213 -0
- package/src/run/schemas.ts +242 -0
- package/src/synth/compose-from-recording.ts +277 -43
- package/src/synth/deny-cases.ts +466 -0
- package/src/synth/evaluate.ts +493 -0
- package/src/synth/harness.ts +40 -0
- package/src/synth/index.ts +12 -0
- package/src/synth/minimize.ts +44 -0
- package/src/synth/predicate-literals.ts +27 -0
- package/src/synth/synthesize-from-recording.ts +655 -19
- package/src/types.ts +38 -2
- package/src/verify/envelope.ts +28 -0
- package/src/verify/index.ts +5 -0
- package/src/verify/simulate.ts +311 -0
- package/src/verify/verify.ts +243 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/synth/evaluate.ts - the model evaluator.
|
|
3
|
+
//
|
|
4
|
+
// Pure function. The single semantic evaluator every deny-case runs through
|
|
5
|
+
// (TS model of the interpreter's semantics; real-semantics wiring is Phase 03).
|
|
6
|
+
// Determinism: same `(predicate, ctx)` -> byte-identical result, no clock, no
|
|
7
|
+
// randomness.
|
|
8
|
+
//
|
|
9
|
+
// Deny order (deny on FIRST violation, stable `reason` string):
|
|
10
|
+
// 1. `atLedger > validUntilLedger` (when set) -> 'EXPIRED'
|
|
11
|
+
// 2. a `now` leaf compared past `valid_until` semantics -> 'EXPIRED'
|
|
12
|
+
// 3. a `call_contract` eq whose literal != ctx.contract -> 'CONTRACT_SCOPE'
|
|
13
|
+
// 4. per-ScVal equality on `call_fn` / `call_arg[i]`;
|
|
14
|
+
// `eq(selector, literal_vec)` = EXACT ordered vector equality
|
|
15
|
+
// (compare element-by-element, deny if length or any differs);
|
|
16
|
+
// fail-closed on opaque/undecodable args -> 'ARG_MISMATCH' / 'FN_MISMATCH'
|
|
17
|
+
// 5. `in` membership: needle NOT in haystack; an EMPTY
|
|
18
|
+
// haystack ALWAYS denies -> 'NOT_IN_ALLOWLIST'
|
|
19
|
+
// 6. amount / window_spent comparisons via BigInt -> 'AMOUNT_BOUND'
|
|
20
|
+
// 7. `invocation_count_in_window` compare -> 'FREQUENCY'
|
|
21
|
+
// 8. `oracle_price`: missing/stale/deviation/etc. is FATAL -
|
|
22
|
+
// throw `OracleError`, catch at the top, deny with the
|
|
23
|
+
// underlying `ORACLE_*` reason -> 'ORACLE_*'
|
|
24
|
+
// 9. boolean nodes: `and` = all children permit;
|
|
25
|
+
// `or` = any child permits; `not` = invert a non-oracle child
|
|
26
|
+
// 10. signer threshold gate (when `signerWeights` provided,
|
|
27
|
+
// no positive-weight signer -> deny 'THRESHOLD_NOT_MET')
|
|
28
|
+
// 11. otherwise `{ permit: true }`.
|
|
29
|
+
//
|
|
30
|
+
// Amounts: BigInt on decimal strings (never JS `number`). Oracle errors
|
|
31
|
+
// modelled by throwing (never a boolean-false that `not`/`or` could mask).
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.evaluate = evaluate;
|
|
34
|
+
const predicate_literals_ts_1 = require("./predicate-literals.js");
|
|
35
|
+
/** Internal fatal thrown by the oracle path; caught at the top of `evaluate`
|
|
36
|
+
* and converted to the matching `ORACLE_*` deny reason. NOT a
|
|
37
|
+
* boolean-false that `not` / `or` could mask. */
|
|
38
|
+
class OracleError extends Error {
|
|
39
|
+
code;
|
|
40
|
+
constructor(code) {
|
|
41
|
+
super(`oracle fatal: ${code}`);
|
|
42
|
+
this.code = code;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const ORACLE_ERROR_CODES = {
|
|
46
|
+
stale: 'ORACLE_STALE',
|
|
47
|
+
missing: 'ORACLE_MISSING',
|
|
48
|
+
deviation: 'ORACLE_DEVIATION_EXCEEDED',
|
|
49
|
+
paused: 'ORACLE_PAUSED',
|
|
50
|
+
decimals: 'ORACLE_DECIMALS_MISMATCH',
|
|
51
|
+
fingerprint: 'ORACLE_FINGERPRINT_DRIFT',
|
|
52
|
+
};
|
|
53
|
+
/** Evaluate a `PredicateNode` against the candidate call described by `ctx`.
|
|
54
|
+
* Pure function. Returns `{ permit: true }` or `{ permit: false; reason }`. */
|
|
55
|
+
function evaluate(predicate, ctx) {
|
|
56
|
+
try {
|
|
57
|
+
// --- step 1: ledger-time expiry ---
|
|
58
|
+
if (ctx.validUntilLedger !== undefined && ctx.atLedger > ctx.validUntilLedger) {
|
|
59
|
+
return { permit: false, reason: 'EXPIRED' };
|
|
60
|
+
}
|
|
61
|
+
// --- step 2..9: predicate tree ---
|
|
62
|
+
const decision = walk(predicate, ctx);
|
|
63
|
+
// --- step 10: signer threshold gate (only when the predicate permitted) ---
|
|
64
|
+
if (decision.permit && ctx.signerWeights !== undefined) {
|
|
65
|
+
let totalWeight = 0n;
|
|
66
|
+
for (const w of Object.values(ctx.signerWeights)) {
|
|
67
|
+
// weights are non-negative integers; BigInt keeps the gate bounded
|
|
68
|
+
totalWeight += BigInt(w);
|
|
69
|
+
}
|
|
70
|
+
if (totalWeight === 0n)
|
|
71
|
+
return { permit: false, reason: 'THRESHOLD_NOT_MET' };
|
|
72
|
+
}
|
|
73
|
+
return decision;
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
if (e instanceof OracleError) {
|
|
77
|
+
return { permit: false, reason: e.code };
|
|
78
|
+
}
|
|
79
|
+
throw e;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Walk the predicate tree. Returns the FIRST deny reason encountered on
|
|
83
|
+
* the active branch (so `and` fails-fast; `or` accepts the first permit). */
|
|
84
|
+
function walk(node, ctx) {
|
|
85
|
+
switch (node.op) {
|
|
86
|
+
case 'and': {
|
|
87
|
+
let lastDeny = null;
|
|
88
|
+
for (const child of node.children) {
|
|
89
|
+
const r = walk(child, ctx);
|
|
90
|
+
if (!r.permit) {
|
|
91
|
+
// deny-on-first: short-circuit. The "active branch" is the failing
|
|
92
|
+
// child, so the reason we surface is from that child.
|
|
93
|
+
return r;
|
|
94
|
+
}
|
|
95
|
+
// keep a reference to the last permit so unused-variable analysis
|
|
96
|
+
// doesn't complain; this is purely structural.
|
|
97
|
+
lastDeny = r;
|
|
98
|
+
}
|
|
99
|
+
return lastDeny ?? { permit: true };
|
|
100
|
+
}
|
|
101
|
+
case 'or': {
|
|
102
|
+
let lastDeny = null;
|
|
103
|
+
for (const child of node.children) {
|
|
104
|
+
const r = walk(child, ctx);
|
|
105
|
+
if (r.permit)
|
|
106
|
+
return r;
|
|
107
|
+
lastDeny = r;
|
|
108
|
+
}
|
|
109
|
+
return lastDeny ?? { permit: false, reason: 'NOT_IN_ALLOWLIST' };
|
|
110
|
+
}
|
|
111
|
+
case 'not': {
|
|
112
|
+
// `not` structurally inverts the child UNLESS the child contains an
|
|
113
|
+
// oracle leaf (compile-time rule: no oracle leaf under not/or). Here we
|
|
114
|
+
// walk the child; if it throws `OracleError`, the catch at the top of
|
|
115
|
+
// `evaluate` re-throws and surfaces the ORACLE_* deny. A non-oracle
|
|
116
|
+
// deny is inverted to a permit, and a permit is inverted to a deny.
|
|
117
|
+
const r = walk(node.child, ctx);
|
|
118
|
+
if (r.permit)
|
|
119
|
+
return { permit: false, reason: 'FN_MISMATCH' };
|
|
120
|
+
return { permit: true };
|
|
121
|
+
}
|
|
122
|
+
case 'eq':
|
|
123
|
+
case 'lt':
|
|
124
|
+
case 'lte':
|
|
125
|
+
case 'gt':
|
|
126
|
+
case 'gte':
|
|
127
|
+
return evalCompare(node.op, node.left, node.right, ctx);
|
|
128
|
+
case 'in':
|
|
129
|
+
return evalIn(node.needle, node.haystack, ctx);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Step 2..8: comparison leaf evaluation. */
|
|
133
|
+
function evalCompare(op, left, right, ctx) {
|
|
134
|
+
// --- step 2: `now` vs `valid_until` semantics ---
|
|
135
|
+
if (left.kind === 'now' && right.kind === 'valid_until') {
|
|
136
|
+
const expired = op === 'gt' || op === 'gte' ? ctx.nowSeconds >= 0 : ctx.nowSeconds < 0;
|
|
137
|
+
// only `gt` / `gte` / `lt` / `lte` are meaningful here. Treat `eq` /
|
|
138
|
+
// any other combo as: "expired iff nowSeconds > validUntilSeconds".
|
|
139
|
+
// We model `valid_until` as a synthetic future timestamp far past
|
|
140
|
+
// `nowSeconds` so the only true-positive expired path is the
|
|
141
|
+
// `gt`/`gte` shapes callers actually write.
|
|
142
|
+
if (op === 'gt' || op === 'gte') {
|
|
143
|
+
if (ctx.nowSeconds > 0)
|
|
144
|
+
return { permit: false, reason: 'EXPIRED' };
|
|
145
|
+
}
|
|
146
|
+
else if (op === 'lt' || op === 'lte') {
|
|
147
|
+
// permits when valid_until is in the future
|
|
148
|
+
return { permit: true };
|
|
149
|
+
}
|
|
150
|
+
return expired ? { permit: false, reason: 'EXPIRED' } : { permit: true };
|
|
151
|
+
}
|
|
152
|
+
if (right.kind === 'now' && left.kind === 'valid_until') {
|
|
153
|
+
return evalCompare(op, right, left, ctx);
|
|
154
|
+
}
|
|
155
|
+
// --- step 3: CONTRACT_SCOPE on call_contract eq ---
|
|
156
|
+
if (left.kind === 'call_contract' && op === 'eq') {
|
|
157
|
+
if (right.kind !== 'literal_address')
|
|
158
|
+
return { permit: false, reason: 'CONTRACT_SCOPE' };
|
|
159
|
+
return right.value === ctx.contract
|
|
160
|
+
? { permit: true }
|
|
161
|
+
: { permit: false, reason: 'CONTRACT_SCOPE' };
|
|
162
|
+
}
|
|
163
|
+
// --- step 4a: call_fn equality ---
|
|
164
|
+
if (left.kind === 'call_fn' && op === 'eq') {
|
|
165
|
+
if (right.kind !== 'literal_symbol')
|
|
166
|
+
return { permit: false, reason: 'FN_MISMATCH' };
|
|
167
|
+
return right.value === ctx.fn ? { permit: true } : { permit: false, reason: 'FN_MISMATCH' };
|
|
168
|
+
}
|
|
169
|
+
// --- step 4b: call_arg comparison (eq / exact-vec, or an ordered numeric bound) ---
|
|
170
|
+
if (left.kind === 'call_arg') {
|
|
171
|
+
const actual = ctx.args[left.index];
|
|
172
|
+
// An ordered comparison (lt/lte/gt/gte) reads the arg as an integer and
|
|
173
|
+
// compares it to a numeric literal via BigInt (e.g. a SoroSwap input-amount
|
|
174
|
+
// cap `call_arg[0] <= limit`). The per-ScVal-type equality semantics live
|
|
175
|
+
// in evalArgEq (`eq` only); ordered ops are numeric-only and fail closed on
|
|
176
|
+
// a non-numeric arg or literal.
|
|
177
|
+
if (op !== 'eq')
|
|
178
|
+
return evalArgOrderedCompare(op, actual, right);
|
|
179
|
+
return evalArgEq(op, actual, right, ctx);
|
|
180
|
+
}
|
|
181
|
+
// --- step 6: AMOUNT_BOUND ---
|
|
182
|
+
if (left.kind === 'amount' && op !== 'eq') {
|
|
183
|
+
return evalAmountCompare(op, left.token, right, ctx);
|
|
184
|
+
}
|
|
185
|
+
if (left.kind === 'window_spent' && op !== 'eq') {
|
|
186
|
+
return evalWindowSpentCompare(op, left.token, right, ctx);
|
|
187
|
+
}
|
|
188
|
+
// `eq` on amount / window_spent not defined as a bound - fall through.
|
|
189
|
+
// --- step 7: FREQUENCY ---
|
|
190
|
+
if (left.kind === 'invocation_count_in_window') {
|
|
191
|
+
return evalFrequencyCompare(op, left.windowSecs, right, ctx);
|
|
192
|
+
}
|
|
193
|
+
// --- step 8: oracle_price (FATAL via throw) ---
|
|
194
|
+
if (left.kind === 'oracle_price') {
|
|
195
|
+
return evalOracleCompare(op, left.asset, right, ctx);
|
|
196
|
+
}
|
|
197
|
+
// Unknown leaf/op combination - structural fail-closed.
|
|
198
|
+
return { permit: false, reason: 'FN_MISMATCH' };
|
|
199
|
+
}
|
|
200
|
+
/** Step 4b: per-ScVal equality. Handles literal_vec as an EXACT ordered
|
|
201
|
+
* sequence: compare element-by-element in order; deny if length or any
|
|
202
|
+
* element differs. Opaque args (`type: 'other'`) fail closed. */
|
|
203
|
+
function evalArgEq(op, actual, right, ctx) {
|
|
204
|
+
// eq(call_arg[i], literal_vec) -> EXACT ordered vector equality
|
|
205
|
+
if (op === 'eq' && right.kind === 'literal_vec') {
|
|
206
|
+
if (actual?.type !== 'vec')
|
|
207
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
208
|
+
return compareVecExact(actual.value, right.elements, ctx)
|
|
209
|
+
? { permit: true }
|
|
210
|
+
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
211
|
+
}
|
|
212
|
+
// eq(call_arg[i], literal_address) -> address compare
|
|
213
|
+
if (op === 'eq' && right.kind === 'literal_address') {
|
|
214
|
+
if (!actual)
|
|
215
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
216
|
+
if (actual.type === 'other')
|
|
217
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
218
|
+
return actual.type === 'address' && actual.value === right.value
|
|
219
|
+
? { permit: true }
|
|
220
|
+
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
221
|
+
}
|
|
222
|
+
// eq(call_arg[i], literal_i128) -> BigInt compare
|
|
223
|
+
if (op === 'eq' && right.kind === 'literal_i128') {
|
|
224
|
+
if (actual?.type !== 'i128')
|
|
225
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
226
|
+
return BigInt(actual.value) === BigInt(right.value)
|
|
227
|
+
? { permit: true }
|
|
228
|
+
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
229
|
+
}
|
|
230
|
+
// eq(call_arg[i], literal_symbol)
|
|
231
|
+
if (op === 'eq' && right.kind === 'literal_symbol') {
|
|
232
|
+
if (!actual)
|
|
233
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
234
|
+
return actual.type === 'symbol' && actual.value === right.value
|
|
235
|
+
? { permit: true }
|
|
236
|
+
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
237
|
+
}
|
|
238
|
+
// eq(call_arg[i], literal_u32)
|
|
239
|
+
if (op === 'eq' && right.kind === 'literal_u32') {
|
|
240
|
+
if (!actual)
|
|
241
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
242
|
+
return actual.type === 'u32' && actual.value === String(right.value)
|
|
243
|
+
? { permit: true }
|
|
244
|
+
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
245
|
+
}
|
|
246
|
+
// eq(call_arg[i], literal_u64)
|
|
247
|
+
if (op === 'eq' && right.kind === 'literal_u64') {
|
|
248
|
+
if (!actual)
|
|
249
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
250
|
+
return actual.type === 'u64' && actual.value === right.value
|
|
251
|
+
? { permit: true }
|
|
252
|
+
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
253
|
+
}
|
|
254
|
+
// eq(call_arg[i], literal_bytes)
|
|
255
|
+
if (op === 'eq' && right.kind === 'literal_bytes') {
|
|
256
|
+
if (!actual)
|
|
257
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
258
|
+
return actual.type === 'bytes' && actual.value === right.value
|
|
259
|
+
? { permit: true }
|
|
260
|
+
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
261
|
+
}
|
|
262
|
+
// Anything else: fail closed on opacity (cannot decode the arg reliably).
|
|
263
|
+
if (!actual || actual.type === 'other')
|
|
264
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
265
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
266
|
+
}
|
|
267
|
+
/** Ordered numeric comparison (lt/lte/gt/gte) on a `call_arg`. The interpreter
|
|
268
|
+
* reads the arg as an integer (i128 / u64 / u32 on the recorder's ScVal
|
|
269
|
+
* surface) and compares it to a numeric literal via BigInt. A non-numeric arg
|
|
270
|
+
* or a non-numeric literal fails closed (ARG_MISMATCH) rather than permitting
|
|
271
|
+
* an undecidable bound. Backs the SoroSwap input-amount cap
|
|
272
|
+
* (`call_arg[0] <= limit`). */
|
|
273
|
+
function evalArgOrderedCompare(op, actual, right) {
|
|
274
|
+
const actualInt = argNumericBigInt(actual);
|
|
275
|
+
const literalInt = (0, predicate_literals_ts_1.literalNumericBigInt)(right);
|
|
276
|
+
if (actualInt === null || literalInt === null) {
|
|
277
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
278
|
+
}
|
|
279
|
+
return bigintCmp(op, actualInt.toString(), literalInt.toString())
|
|
280
|
+
? { permit: true }
|
|
281
|
+
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
282
|
+
}
|
|
283
|
+
/** BigInt value of a numeric-integer ScVal arg (i128 / u64 / u32), or null when
|
|
284
|
+
* the arg is absent, opaque, or a non-numeric type. */
|
|
285
|
+
function argNumericBigInt(actual) {
|
|
286
|
+
if (!actual)
|
|
287
|
+
return null;
|
|
288
|
+
if (actual.type === 'i128' || actual.type === 'u64' || actual.type === 'u32') {
|
|
289
|
+
try {
|
|
290
|
+
return BigInt(actual.value);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
/** Element-by-element ordered comparison of an `ScVal[]` against a
|
|
299
|
+
* `PredicateLeaf[]`. Equal-length and equal at every index => permit.
|
|
300
|
+
* Length mismatch OR any element mismatch => deny ARG_MISMATCH. */
|
|
301
|
+
function compareVecExact(actual, expected, ctx) {
|
|
302
|
+
if (actual.length !== expected.length)
|
|
303
|
+
return false;
|
|
304
|
+
for (let i = 0; i < expected.length; i++) {
|
|
305
|
+
const e = expected[i];
|
|
306
|
+
const a = actual[i];
|
|
307
|
+
if (!e || !a)
|
|
308
|
+
return false;
|
|
309
|
+
const r = evalArgEq('eq', a, e, ctx);
|
|
310
|
+
if (!r.permit)
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
/** Step 5: `in` membership. Empty haystack ALWAYS denies. Opaque needle
|
|
316
|
+
* fails closed. */
|
|
317
|
+
function evalIn(needle, haystack, ctx) {
|
|
318
|
+
if (haystack.length === 0)
|
|
319
|
+
return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
|
|
320
|
+
// Resolve the needle's ScVal against the candidate call.
|
|
321
|
+
const actual = resolveLeaf(needle, ctx);
|
|
322
|
+
if (!actual || actual.type === 'other')
|
|
323
|
+
return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
|
|
324
|
+
for (const h of haystack) {
|
|
325
|
+
const r = evalArgEq('eq', actual, h, ctx);
|
|
326
|
+
if (r.permit)
|
|
327
|
+
return { permit: true };
|
|
328
|
+
}
|
|
329
|
+
return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
|
|
330
|
+
}
|
|
331
|
+
/** Resolve a selector leaf to its current ScVal against the candidate call. */
|
|
332
|
+
function resolveLeaf(leaf, ctx) {
|
|
333
|
+
switch (leaf.kind) {
|
|
334
|
+
case 'call_contract':
|
|
335
|
+
return { type: 'address', value: ctx.contract };
|
|
336
|
+
case 'call_fn':
|
|
337
|
+
return { type: 'symbol', value: ctx.fn };
|
|
338
|
+
case 'call_arg':
|
|
339
|
+
return ctx.args[leaf.index];
|
|
340
|
+
case 'amount':
|
|
341
|
+
case 'window_spent':
|
|
342
|
+
case 'oracle_price':
|
|
343
|
+
case 'invocation_count_in_window':
|
|
344
|
+
case 'now':
|
|
345
|
+
case 'valid_until':
|
|
346
|
+
return undefined; // selector leaves with no ScVal projection
|
|
347
|
+
case 'literal_address':
|
|
348
|
+
return { type: 'address', value: leaf.value };
|
|
349
|
+
case 'literal_i128':
|
|
350
|
+
return { type: 'i128', value: leaf.value };
|
|
351
|
+
case 'literal_symbol':
|
|
352
|
+
return { type: 'symbol', value: leaf.value };
|
|
353
|
+
case 'literal_u32':
|
|
354
|
+
return { type: 'u32', value: String(leaf.value) };
|
|
355
|
+
case 'literal_u64':
|
|
356
|
+
return { type: 'u64', value: leaf.value };
|
|
357
|
+
case 'literal_bytes':
|
|
358
|
+
return { type: 'bytes', value: leaf.value };
|
|
359
|
+
case 'literal_vec':
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/** Step 6: amount compare on BigInt. */
|
|
364
|
+
function evalAmountCompare(op, token, right, ctx) {
|
|
365
|
+
const literal = right.kind === 'literal_i128' ? right.value : null;
|
|
366
|
+
if (literal === null)
|
|
367
|
+
return { permit: false, reason: 'AMOUNT_BOUND' };
|
|
368
|
+
const actual = ctx.amountByToken[token] ?? '0';
|
|
369
|
+
return bigintCmp(op, actual, literal)
|
|
370
|
+
? { permit: true }
|
|
371
|
+
: { permit: false, reason: 'AMOUNT_BOUND' };
|
|
372
|
+
}
|
|
373
|
+
/** Step 6: window_spent compare on BigInt. */
|
|
374
|
+
function evalWindowSpentCompare(op, token, right, ctx) {
|
|
375
|
+
const literal = right.kind === 'literal_i128' ? right.value : null;
|
|
376
|
+
if (literal === null)
|
|
377
|
+
return { permit: false, reason: 'AMOUNT_BOUND' };
|
|
378
|
+
const actual = ctx.windowSpentByToken[token] ?? '0';
|
|
379
|
+
return bigintCmp(op, actual, literal)
|
|
380
|
+
? { permit: true }
|
|
381
|
+
: { permit: false, reason: 'AMOUNT_BOUND' };
|
|
382
|
+
}
|
|
383
|
+
/** Step 7: invocation_count_in_window compare. */
|
|
384
|
+
function evalFrequencyCompare(op, windowSecs, right, ctx) {
|
|
385
|
+
const literal = right.kind === 'literal_u32' ? String(right.value) : null;
|
|
386
|
+
if (literal === null)
|
|
387
|
+
return { permit: false, reason: 'FREQUENCY' };
|
|
388
|
+
const actual = String(ctx.invocationCountByWindow[windowSecs] ?? 0);
|
|
389
|
+
return bigintCmp(op, actual, literal) ? { permit: true } : { permit: false, reason: 'FREQUENCY' };
|
|
390
|
+
}
|
|
391
|
+
/** Step 8: oracle_price compare. Reads `ctx.oraclePriceByAsset[asset]`. Any
|
|
392
|
+
* error entry OR a missing key throws `OracleError` (FATAL). A satisfied
|
|
393
|
+
* compare permits. */
|
|
394
|
+
function evalOracleCompare(op, asset, right, ctx) {
|
|
395
|
+
const entry = ctx.oraclePriceByAsset[asset];
|
|
396
|
+
if (!entry)
|
|
397
|
+
throw new OracleError('ORACLE_STALE');
|
|
398
|
+
if ('error' in entry) {
|
|
399
|
+
const mapped = ORACLE_ERROR_CODES[entry.error];
|
|
400
|
+
if (!mapped)
|
|
401
|
+
throw new OracleError('ORACLE_STALE');
|
|
402
|
+
throw new OracleError(mapped);
|
|
403
|
+
}
|
|
404
|
+
const literal = right.kind === 'literal_i128' ? right.value : null;
|
|
405
|
+
if (literal === null)
|
|
406
|
+
throw new OracleError('ORACLE_DECIMALS_MISMATCH');
|
|
407
|
+
return bigintCmp(op, entry.price, literal)
|
|
408
|
+
? { permit: true }
|
|
409
|
+
: { permit: false, reason: 'FN_MISMATCH' };
|
|
410
|
+
}
|
|
411
|
+
/** BigInt compare helper. `eq` is also supported by callers (selector-vs-literal
|
|
412
|
+
* equal checks), but the dedicated arg path uses it via `BigInt(actual) === BigInt(right)`. */
|
|
413
|
+
function bigintCmp(op, aStr, bStr) {
|
|
414
|
+
const a = BigInt(aStr);
|
|
415
|
+
const b = BigInt(bStr);
|
|
416
|
+
switch (op) {
|
|
417
|
+
case 'eq':
|
|
418
|
+
return a === b;
|
|
419
|
+
case 'lt':
|
|
420
|
+
return a < b;
|
|
421
|
+
case 'lte':
|
|
422
|
+
return a <= b;
|
|
423
|
+
case 'gt':
|
|
424
|
+
return a > b;
|
|
425
|
+
case 'gte':
|
|
426
|
+
return a >= b;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PredicateNode } from '../types.ts';
|
|
2
|
+
import type { GeneratedCases } from './deny-cases.ts';
|
|
3
|
+
export interface HarnessFailure {
|
|
4
|
+
dimension: string;
|
|
5
|
+
expected: 'permit' | 'deny';
|
|
6
|
+
got: 'permit' | 'deny';
|
|
7
|
+
reason?: string;
|
|
8
|
+
}
|
|
9
|
+
export type HarnessResult = {
|
|
10
|
+
ok: true;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
failures: HarnessFailure[];
|
|
14
|
+
};
|
|
15
|
+
/** Evaluate the intended call and every generated denial without side effects. */
|
|
16
|
+
export declare function runHarness(predicate: PredicateNode, cases: GeneratedCases): HarnessResult;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runHarness = runHarness;
|
|
4
|
+
const evaluate_ts_1 = require("./evaluate.js");
|
|
5
|
+
/** Evaluate the intended call and every generated denial without side effects. */
|
|
6
|
+
function runHarness(predicate, cases) {
|
|
7
|
+
const failures = [];
|
|
8
|
+
const permitResult = (0, evaluate_ts_1.evaluate)(predicate, cases.permit);
|
|
9
|
+
if (!permitResult.permit) {
|
|
10
|
+
failures.push({
|
|
11
|
+
dimension: 'PERMIT_CASE_FAILED',
|
|
12
|
+
expected: 'permit',
|
|
13
|
+
got: 'deny',
|
|
14
|
+
reason: permitResult.reason,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
for (const deny of cases.denies) {
|
|
18
|
+
const result = (0, evaluate_ts_1.evaluate)(predicate, deny.ctx);
|
|
19
|
+
if (result.permit) {
|
|
20
|
+
failures.push({
|
|
21
|
+
dimension: deny.dimension,
|
|
22
|
+
expected: 'deny',
|
|
23
|
+
got: 'permit',
|
|
24
|
+
reason: 'DENY_CASE_FAILURE',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return failures.length === 0 ? { ok: true } : { ok: false, failures };
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { type ComposeOptions, type ComposeResult, composeFromRecording, } from './compose-from-recording.ts';
|
|
2
|
+
export { type DenyCase, type GeneratedCases, generateCases, } from './deny-cases.ts';
|
|
3
|
+
export { type EvalContext, type EvalResult, evaluate } from './evaluate.ts';
|
|
4
|
+
export { type HarnessFailure, type HarnessResult, runHarness, } from './harness.ts';
|
|
5
|
+
export { type IntentFacts, lower } from './lower.ts';
|
|
6
|
+
export { minimize } from './minimize.ts';
|
|
7
|
+
export { type DecideScopeOptions, decideScope, type ScopeDecision, scopeToContextRuleType, } from './scope.ts';
|
|
8
|
+
export { synthesizeFromMandate } from './synthesize-from-mandate.ts';
|
|
9
|
+
export { type SynthesizeFromRecordingOptions, synthesizeFromRecording, } from './synthesize-from-recording.ts';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/synth/index.ts - re-export the synthesizer front-ends.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.synthesizeFromRecording = exports.synthesizeFromMandate = exports.scopeToContextRuleType = exports.decideScope = exports.minimize = exports.lower = exports.runHarness = exports.evaluate = exports.generateCases = exports.composeFromRecording = void 0;
|
|
5
|
+
var compose_from_recording_ts_1 = require("./compose-from-recording.js");
|
|
6
|
+
Object.defineProperty(exports, "composeFromRecording", { enumerable: true, get: function () { return compose_from_recording_ts_1.composeFromRecording; } });
|
|
7
|
+
var deny_cases_ts_1 = require("./deny-cases.js");
|
|
8
|
+
Object.defineProperty(exports, "generateCases", { enumerable: true, get: function () { return deny_cases_ts_1.generateCases; } });
|
|
9
|
+
var evaluate_ts_1 = require("./evaluate.js");
|
|
10
|
+
Object.defineProperty(exports, "evaluate", { enumerable: true, get: function () { return evaluate_ts_1.evaluate; } });
|
|
11
|
+
var harness_ts_1 = require("./harness.js");
|
|
12
|
+
Object.defineProperty(exports, "runHarness", { enumerable: true, get: function () { return harness_ts_1.runHarness; } });
|
|
13
|
+
var lower_ts_1 = require("./lower.js");
|
|
14
|
+
Object.defineProperty(exports, "lower", { enumerable: true, get: function () { return lower_ts_1.lower; } });
|
|
15
|
+
var minimize_ts_1 = require("./minimize.js");
|
|
16
|
+
Object.defineProperty(exports, "minimize", { enumerable: true, get: function () { return minimize_ts_1.minimize; } });
|
|
17
|
+
var scope_ts_1 = require("./scope.js");
|
|
18
|
+
Object.defineProperty(exports, "decideScope", { enumerable: true, get: function () { return scope_ts_1.decideScope; } });
|
|
19
|
+
Object.defineProperty(exports, "scopeToContextRuleType", { enumerable: true, get: function () { return scope_ts_1.scopeToContextRuleType; } });
|
|
20
|
+
var synthesize_from_mandate_ts_1 = require("./synthesize-from-mandate.js");
|
|
21
|
+
Object.defineProperty(exports, "synthesizeFromMandate", { enumerable: true, get: function () { return synthesize_from_mandate_ts_1.synthesizeFromMandate; } });
|
|
22
|
+
var synthesize_from_recording_ts_1 = require("./synthesize-from-recording.js");
|
|
23
|
+
Object.defineProperty(exports, "synthesizeFromRecording", { enumerable: true, get: function () { return synthesize_from_recording_ts_1.synthesizeFromRecording; } });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RecordedTransaction } from '../types.ts';
|
|
2
|
+
/** Facts the recording-path synth reasons over. */
|
|
3
|
+
export interface IntentFacts {
|
|
4
|
+
callTargets: string[];
|
|
5
|
+
functionsByContract: Record<string, string[]>;
|
|
6
|
+
/** Aggregate outgoing spend per token (i128-safe decimal string). A single
|
|
7
|
+
* token entry is the prerequisite for `spending_limit`. Movements
|
|
8
|
+
* with from != sourceAccount are NOT counted as "spend" by this synth
|
|
9
|
+
* (incoming yield, refund, etc.). */
|
|
10
|
+
spendByToken: Record<string, string>;
|
|
11
|
+
signers: string[];
|
|
12
|
+
/** Set when more than one top-level invocation shares a target contract
|
|
13
|
+
* (a router); decideScope uses this to scope by the router instead of
|
|
14
|
+
* returning SCOPE_UNRESOLVED. */
|
|
15
|
+
sharedRouter?: string;
|
|
16
|
+
/** Router hop paths, keyed by target contract. SoroSwap exposes its `path`
|
|
17
|
+
* arg directly on the top-level call; this is the input the compose step
|
|
18
|
+
* passes to a per-arg `in` condition (flagged as not covered by the OZ adapter). */
|
|
19
|
+
allowedPaths?: Record<string, string[][]>;
|
|
20
|
+
}
|
|
21
|
+
/** Lower a recorded transaction to the canonical IntentFacts. Pure (no
|
|
22
|
+
* randomness, no clock); same `RecordedTransaction` -> byte-identical facts. */
|
|
23
|
+
export declare function lower(tx: RecordedTransaction): IntentFacts;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/synth/lower.ts - lower a RecordedTransaction to IntentFacts.
|
|
3
|
+
//
|
|
4
|
+
// Reads the top-level invocations only (the single authorized call that
|
|
5
|
+
// `Policy::enforce` receives; v1 grammar does not walk sub-invocations) and
|
|
6
|
+
// the recorded token movements. Produces the deterministic facts the synth
|
|
7
|
+
// reasons over: distinct call targets, functions per target, cumulative spend
|
|
8
|
+
// per token (outgoing movements from the recorded source account only),
|
|
9
|
+
// authorising signers, an optional shared-router marker when multiple top-level
|
|
10
|
+
// invocations hit the same contract, and the per-contract hop-path lists for
|
|
11
|
+
// router calls (SoroSwap `path`).
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.lower = lower;
|
|
14
|
+
/** Lower a recorded transaction to the canonical IntentFacts. Pure (no
|
|
15
|
+
* randomness, no clock); same `RecordedTransaction` -> byte-identical facts. */
|
|
16
|
+
function lower(tx) {
|
|
17
|
+
const invocations = tx.invocations;
|
|
18
|
+
const callTargets = uniqueOrdered(invocations.map((i) => i.contract));
|
|
19
|
+
const functionsByContract = groupFunctionsByContract(invocations);
|
|
20
|
+
const spendByToken = aggregateOutgoingSpend(tx.tokenMovements, tx.sourceAccount);
|
|
21
|
+
const allowedPaths = extractPathsByContract(invocations);
|
|
22
|
+
const sharedRouter = inferSharedRouter(invocations);
|
|
23
|
+
const facts = {
|
|
24
|
+
callTargets,
|
|
25
|
+
functionsByContract,
|
|
26
|
+
spendByToken,
|
|
27
|
+
signers: [...tx.signers],
|
|
28
|
+
};
|
|
29
|
+
if (sharedRouter)
|
|
30
|
+
facts.sharedRouter = sharedRouter;
|
|
31
|
+
if (Object.keys(allowedPaths).length > 0)
|
|
32
|
+
facts.allowedPaths = allowedPaths;
|
|
33
|
+
return facts;
|
|
34
|
+
}
|
|
35
|
+
/** Preserve first-seen ordering, drop duplicates. */
|
|
36
|
+
function uniqueOrdered(items) {
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const item of items) {
|
|
40
|
+
if (seen.has(item))
|
|
41
|
+
continue;
|
|
42
|
+
seen.add(item);
|
|
43
|
+
out.push(item);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** Group the function names invoked per top-level contract. Order preserved
|
|
48
|
+
* per contract in first-seen order. */
|
|
49
|
+
function groupFunctionsByContract(invocations) {
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const inv of invocations) {
|
|
52
|
+
const existing = out[inv.contract] ?? [];
|
|
53
|
+
if (!existing.includes(inv.fn))
|
|
54
|
+
existing.push(inv.fn);
|
|
55
|
+
out[inv.contract] = existing;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/** Sum outgoing TokenMovement amounts per token, where `from === source`.
|
|
60
|
+
* BigInt throughout; never lossy. Movements whose `from` does not match the
|
|
61
|
+
* recorded source account are NOT counted (incoming yield, refund, etc.). */
|
|
62
|
+
function aggregateOutgoingSpend(movements, sourceAccount) {
|
|
63
|
+
const totals = new Map();
|
|
64
|
+
for (const m of movements) {
|
|
65
|
+
if (m.from !== sourceAccount)
|
|
66
|
+
continue;
|
|
67
|
+
const current = totals.get(m.token) ?? 0n;
|
|
68
|
+
totals.set(m.token, current + BigInt(m.amount));
|
|
69
|
+
}
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const [token, total] of totals) {
|
|
72
|
+
out[token] = total.toString();
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/** Walk the top-level invocations looking for a `vec` arg whose elements are
|
|
77
|
+
* addresses (the SoroSwap `path` shape). Only the FIRST arg in each
|
|
78
|
+
* invocation is examined in v1 - the recorder emits the top-level args
|
|
79
|
+
* in declaration order and the only contract-call convention we bind by is
|
|
80
|
+
* the router's first vec-arg path. */
|
|
81
|
+
function extractPathsByContract(invocations) {
|
|
82
|
+
const out = {};
|
|
83
|
+
for (const inv of invocations) {
|
|
84
|
+
const path = findAddressVec(inv.args);
|
|
85
|
+
if (path) {
|
|
86
|
+
const existing = out[inv.contract] ?? [];
|
|
87
|
+
existing.push(path);
|
|
88
|
+
out[inv.contract] = existing;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function findAddressVec(args) {
|
|
94
|
+
for (const arg of args) {
|
|
95
|
+
if (arg.type === 'vec') {
|
|
96
|
+
const addresses = arg.value.filter((v) => v.type === 'address');
|
|
97
|
+
if (addresses.length === arg.value.length && addresses.length > 0) {
|
|
98
|
+
return addresses.map((a) => a.value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/** When multiple top-level invocations share the same target contract, that
|
|
105
|
+
* contract is the shared router. Single-call transactions return undefined
|
|
106
|
+
* (the default flow doesn't need the marker). */
|
|
107
|
+
function inferSharedRouter(invocations) {
|
|
108
|
+
if (invocations.length < 2)
|
|
109
|
+
return undefined;
|
|
110
|
+
const counts = new Map();
|
|
111
|
+
for (const inv of invocations) {
|
|
112
|
+
counts.set(inv.contract, (counts.get(inv.contract) ?? 0) + 1);
|
|
113
|
+
}
|
|
114
|
+
for (const [contract, count] of counts) {
|
|
115
|
+
if (count >= 2)
|
|
116
|
+
return contract;
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PredicateNode } from '../types.ts';
|
|
2
|
+
import type { EvalContext } from './evaluate.ts';
|
|
3
|
+
/** Remove top-level conjuncts only when the current and regenerated batteries still deny. */
|
|
4
|
+
export declare function minimize(predicate: PredicateNode, permitCtx: EvalContext): PredicateNode;
|