@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,493 @@
|
|
|
1
|
+
// src/synth/evaluate.ts - the model evaluator.
|
|
2
|
+
//
|
|
3
|
+
// Pure function. The single semantic evaluator every deny-case runs through
|
|
4
|
+
// (TS model of the interpreter's semantics; real-semantics wiring is Phase 03).
|
|
5
|
+
// Determinism: same `(predicate, ctx)` -> byte-identical result, no clock, no
|
|
6
|
+
// randomness.
|
|
7
|
+
//
|
|
8
|
+
// Deny order (deny on FIRST violation, stable `reason` string):
|
|
9
|
+
// 1. `atLedger > validUntilLedger` (when set) -> 'EXPIRED'
|
|
10
|
+
// 2. a `now` leaf compared past `valid_until` semantics -> 'EXPIRED'
|
|
11
|
+
// 3. a `call_contract` eq whose literal != ctx.contract -> 'CONTRACT_SCOPE'
|
|
12
|
+
// 4. per-ScVal equality on `call_fn` / `call_arg[i]`;
|
|
13
|
+
// `eq(selector, literal_vec)` = EXACT ordered vector equality
|
|
14
|
+
// (compare element-by-element, deny if length or any differs);
|
|
15
|
+
// fail-closed on opaque/undecodable args -> 'ARG_MISMATCH' / 'FN_MISMATCH'
|
|
16
|
+
// 5. `in` membership: needle NOT in haystack; an EMPTY
|
|
17
|
+
// haystack ALWAYS denies -> 'NOT_IN_ALLOWLIST'
|
|
18
|
+
// 6. amount / window_spent comparisons via BigInt -> 'AMOUNT_BOUND'
|
|
19
|
+
// 7. `invocation_count_in_window` compare -> 'FREQUENCY'
|
|
20
|
+
// 8. `oracle_price`: missing/stale/deviation/etc. is FATAL -
|
|
21
|
+
// throw `OracleError`, catch at the top, deny with the
|
|
22
|
+
// underlying `ORACLE_*` reason -> 'ORACLE_*'
|
|
23
|
+
// 9. boolean nodes: `and` = all children permit;
|
|
24
|
+
// `or` = any child permits; `not` = invert a non-oracle child
|
|
25
|
+
// 10. signer threshold gate (when `signerWeights` provided,
|
|
26
|
+
// no positive-weight signer -> deny 'THRESHOLD_NOT_MET')
|
|
27
|
+
// 11. otherwise `{ permit: true }`.
|
|
28
|
+
//
|
|
29
|
+
// Amounts: BigInt on decimal strings (never JS `number`). Oracle errors
|
|
30
|
+
// modelled by throwing (never a boolean-false that `not`/`or` could mask).
|
|
31
|
+
|
|
32
|
+
import type { PredicateLeaf, PredicateNode, ScVal } from '../types.ts'
|
|
33
|
+
import { literalNumericBigInt } from './predicate-literals.ts'
|
|
34
|
+
|
|
35
|
+
export interface EvalContext {
|
|
36
|
+
/** Contract the interpreter is asked to enforce against. */
|
|
37
|
+
contract: string
|
|
38
|
+
/** Function name on that contract. */
|
|
39
|
+
fn: string
|
|
40
|
+
/** Decoded `ScVal[]` of the top-level authorized call. */
|
|
41
|
+
args: ScVal[]
|
|
42
|
+
/** Ledger sequence at which the interpreter is invoked. */
|
|
43
|
+
atLedger: number
|
|
44
|
+
/** Optional policy expiry; absent -> step 1 is skipped. */
|
|
45
|
+
validUntilLedger?: number
|
|
46
|
+
/** Unix seconds; used by `now` / `valid_until` leaves. */
|
|
47
|
+
nowSeconds: number
|
|
48
|
+
/** Per-token amount moved by the current call (i128 decimal string). */
|
|
49
|
+
amountByToken: Record<string, string>
|
|
50
|
+
/** Per-token window-rolling spend prior to this call (i128 decimal string). */
|
|
51
|
+
windowSpentByToken: Record<string, string>
|
|
52
|
+
/** Recorded invocation counts keyed by window seconds. */
|
|
53
|
+
invocationCountByWindow: Record<number, number>
|
|
54
|
+
/** Optional signer-weight map for the threshold gate. Absent -> skip. */
|
|
55
|
+
signerWeights?: Record<string, number>
|
|
56
|
+
/** Per-asset oracle snapshot. Missing keys default to ORACLE_STALE. */
|
|
57
|
+
oraclePriceByAsset: Record<
|
|
58
|
+
string,
|
|
59
|
+
| { price: string; timestampSeconds: number }
|
|
60
|
+
| { error: 'stale' | 'missing' | 'deviation' | 'paused' | 'decimals' | 'fingerprint' }
|
|
61
|
+
>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type EvalResult = { permit: true } | { permit: false; reason: string }
|
|
65
|
+
|
|
66
|
+
/** Internal fatal thrown by the oracle path; caught at the top of `evaluate`
|
|
67
|
+
* and converted to the matching `ORACLE_*` deny reason. NOT a
|
|
68
|
+
* boolean-false that `not` / `or` could mask. */
|
|
69
|
+
class OracleError extends Error {
|
|
70
|
+
readonly code: string
|
|
71
|
+
constructor(code: string) {
|
|
72
|
+
super(`oracle fatal: ${code}`)
|
|
73
|
+
this.code = code
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ORACLE_ERROR_CODES: Readonly<Record<string, string>> = {
|
|
78
|
+
stale: 'ORACLE_STALE',
|
|
79
|
+
missing: 'ORACLE_MISSING',
|
|
80
|
+
deviation: 'ORACLE_DEVIATION_EXCEEDED',
|
|
81
|
+
paused: 'ORACLE_PAUSED',
|
|
82
|
+
decimals: 'ORACLE_DECIMALS_MISMATCH',
|
|
83
|
+
fingerprint: 'ORACLE_FINGERPRINT_DRIFT',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Evaluate a `PredicateNode` against the candidate call described by `ctx`.
|
|
87
|
+
* Pure function. Returns `{ permit: true }` or `{ permit: false; reason }`. */
|
|
88
|
+
export function evaluate(predicate: PredicateNode, ctx: EvalContext): EvalResult {
|
|
89
|
+
try {
|
|
90
|
+
// --- step 1: ledger-time expiry ---
|
|
91
|
+
if (ctx.validUntilLedger !== undefined && ctx.atLedger > ctx.validUntilLedger) {
|
|
92
|
+
return { permit: false, reason: 'EXPIRED' }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- step 2..9: predicate tree ---
|
|
96
|
+
const decision = walk(predicate, ctx)
|
|
97
|
+
|
|
98
|
+
// --- step 10: signer threshold gate (only when the predicate permitted) ---
|
|
99
|
+
if (decision.permit && ctx.signerWeights !== undefined) {
|
|
100
|
+
let totalWeight = 0n
|
|
101
|
+
for (const w of Object.values(ctx.signerWeights)) {
|
|
102
|
+
// weights are non-negative integers; BigInt keeps the gate bounded
|
|
103
|
+
totalWeight += BigInt(w)
|
|
104
|
+
}
|
|
105
|
+
if (totalWeight === 0n) return { permit: false, reason: 'THRESHOLD_NOT_MET' }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return decision
|
|
109
|
+
} catch (e) {
|
|
110
|
+
if (e instanceof OracleError) {
|
|
111
|
+
return { permit: false, reason: e.code }
|
|
112
|
+
}
|
|
113
|
+
throw e
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Walk the predicate tree. Returns the FIRST deny reason encountered on
|
|
118
|
+
* the active branch (so `and` fails-fast; `or` accepts the first permit). */
|
|
119
|
+
function walk(node: PredicateNode, ctx: EvalContext): EvalResult {
|
|
120
|
+
switch (node.op) {
|
|
121
|
+
case 'and': {
|
|
122
|
+
let lastDeny: EvalResult | null = null
|
|
123
|
+
for (const child of node.children) {
|
|
124
|
+
const r = walk(child, ctx)
|
|
125
|
+
if (!r.permit) {
|
|
126
|
+
// deny-on-first: short-circuit. The "active branch" is the failing
|
|
127
|
+
// child, so the reason we surface is from that child.
|
|
128
|
+
return r
|
|
129
|
+
}
|
|
130
|
+
// keep a reference to the last permit so unused-variable analysis
|
|
131
|
+
// doesn't complain; this is purely structural.
|
|
132
|
+
lastDeny = r
|
|
133
|
+
}
|
|
134
|
+
return lastDeny ?? { permit: true }
|
|
135
|
+
}
|
|
136
|
+
case 'or': {
|
|
137
|
+
let lastDeny: EvalResult | null = null
|
|
138
|
+
for (const child of node.children) {
|
|
139
|
+
const r = walk(child, ctx)
|
|
140
|
+
if (r.permit) return r
|
|
141
|
+
lastDeny = r
|
|
142
|
+
}
|
|
143
|
+
return lastDeny ?? { permit: false, reason: 'NOT_IN_ALLOWLIST' }
|
|
144
|
+
}
|
|
145
|
+
case 'not': {
|
|
146
|
+
// `not` structurally inverts the child UNLESS the child contains an
|
|
147
|
+
// oracle leaf (compile-time rule: no oracle leaf under not/or). Here we
|
|
148
|
+
// walk the child; if it throws `OracleError`, the catch at the top of
|
|
149
|
+
// `evaluate` re-throws and surfaces the ORACLE_* deny. A non-oracle
|
|
150
|
+
// deny is inverted to a permit, and a permit is inverted to a deny.
|
|
151
|
+
const r = walk(node.child, ctx)
|
|
152
|
+
if (r.permit) return { permit: false, reason: 'FN_MISMATCH' }
|
|
153
|
+
return { permit: true }
|
|
154
|
+
}
|
|
155
|
+
case 'eq':
|
|
156
|
+
case 'lt':
|
|
157
|
+
case 'lte':
|
|
158
|
+
case 'gt':
|
|
159
|
+
case 'gte':
|
|
160
|
+
return evalCompare(node.op, node.left, node.right, ctx)
|
|
161
|
+
case 'in':
|
|
162
|
+
return evalIn(node.needle, node.haystack, ctx)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Step 2..8: comparison leaf evaluation. */
|
|
167
|
+
function evalCompare(
|
|
168
|
+
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
169
|
+
left: PredicateLeaf,
|
|
170
|
+
right: PredicateLeaf,
|
|
171
|
+
ctx: EvalContext
|
|
172
|
+
): EvalResult {
|
|
173
|
+
// --- step 2: `now` vs `valid_until` semantics ---
|
|
174
|
+
if (left.kind === 'now' && right.kind === 'valid_until') {
|
|
175
|
+
const expired = op === 'gt' || op === 'gte' ? ctx.nowSeconds >= 0 : ctx.nowSeconds < 0
|
|
176
|
+
// only `gt` / `gte` / `lt` / `lte` are meaningful here. Treat `eq` /
|
|
177
|
+
// any other combo as: "expired iff nowSeconds > validUntilSeconds".
|
|
178
|
+
// We model `valid_until` as a synthetic future timestamp far past
|
|
179
|
+
// `nowSeconds` so the only true-positive expired path is the
|
|
180
|
+
// `gt`/`gte` shapes callers actually write.
|
|
181
|
+
if (op === 'gt' || op === 'gte') {
|
|
182
|
+
if (ctx.nowSeconds > 0) return { permit: false, reason: 'EXPIRED' }
|
|
183
|
+
} else if (op === 'lt' || op === 'lte') {
|
|
184
|
+
// permits when valid_until is in the future
|
|
185
|
+
return { permit: true }
|
|
186
|
+
}
|
|
187
|
+
return expired ? { permit: false, reason: 'EXPIRED' } : { permit: true }
|
|
188
|
+
}
|
|
189
|
+
if (right.kind === 'now' && left.kind === 'valid_until') {
|
|
190
|
+
return evalCompare(op, right, left, ctx)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// --- step 3: CONTRACT_SCOPE on call_contract eq ---
|
|
194
|
+
if (left.kind === 'call_contract' && op === 'eq') {
|
|
195
|
+
if (right.kind !== 'literal_address') return { permit: false, reason: 'CONTRACT_SCOPE' }
|
|
196
|
+
return right.value === ctx.contract
|
|
197
|
+
? { permit: true }
|
|
198
|
+
: { permit: false, reason: 'CONTRACT_SCOPE' }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- step 4a: call_fn equality ---
|
|
202
|
+
if (left.kind === 'call_fn' && op === 'eq') {
|
|
203
|
+
if (right.kind !== 'literal_symbol') return { permit: false, reason: 'FN_MISMATCH' }
|
|
204
|
+
return right.value === ctx.fn ? { permit: true } : { permit: false, reason: 'FN_MISMATCH' }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --- step 4b: call_arg comparison (eq / exact-vec, or an ordered numeric bound) ---
|
|
208
|
+
if (left.kind === 'call_arg') {
|
|
209
|
+
const actual = ctx.args[left.index]
|
|
210
|
+
// An ordered comparison (lt/lte/gt/gte) reads the arg as an integer and
|
|
211
|
+
// compares it to a numeric literal via BigInt (e.g. a SoroSwap input-amount
|
|
212
|
+
// cap `call_arg[0] <= limit`). The per-ScVal-type equality semantics live
|
|
213
|
+
// in evalArgEq (`eq` only); ordered ops are numeric-only and fail closed on
|
|
214
|
+
// a non-numeric arg or literal.
|
|
215
|
+
if (op !== 'eq') return evalArgOrderedCompare(op, actual, right)
|
|
216
|
+
return evalArgEq(op, actual, right, ctx)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// --- step 6: AMOUNT_BOUND ---
|
|
220
|
+
if (left.kind === 'amount' && op !== 'eq') {
|
|
221
|
+
return evalAmountCompare(op, left.token, right, ctx)
|
|
222
|
+
}
|
|
223
|
+
if (left.kind === 'window_spent' && op !== 'eq') {
|
|
224
|
+
return evalWindowSpentCompare(op, left.token, right, ctx)
|
|
225
|
+
}
|
|
226
|
+
// `eq` on amount / window_spent not defined as a bound - fall through.
|
|
227
|
+
|
|
228
|
+
// --- step 7: FREQUENCY ---
|
|
229
|
+
if (left.kind === 'invocation_count_in_window') {
|
|
230
|
+
return evalFrequencyCompare(op, left.windowSecs, right, ctx)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- step 8: oracle_price (FATAL via throw) ---
|
|
234
|
+
if (left.kind === 'oracle_price') {
|
|
235
|
+
return evalOracleCompare(op, left.asset, right, ctx)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Unknown leaf/op combination - structural fail-closed.
|
|
239
|
+
return { permit: false, reason: 'FN_MISMATCH' }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Step 4b: per-ScVal equality. Handles literal_vec as an EXACT ordered
|
|
243
|
+
* sequence: compare element-by-element in order; deny if length or any
|
|
244
|
+
* element differs. Opaque args (`type: 'other'`) fail closed. */
|
|
245
|
+
function evalArgEq(
|
|
246
|
+
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
247
|
+
actual: ScVal | undefined,
|
|
248
|
+
right: PredicateLeaf,
|
|
249
|
+
ctx: EvalContext
|
|
250
|
+
): EvalResult {
|
|
251
|
+
// eq(call_arg[i], literal_vec) -> EXACT ordered vector equality
|
|
252
|
+
if (op === 'eq' && right.kind === 'literal_vec') {
|
|
253
|
+
if (actual?.type !== 'vec') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
254
|
+
return compareVecExact(actual.value, right.elements, ctx)
|
|
255
|
+
? { permit: true }
|
|
256
|
+
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// eq(call_arg[i], literal_address) -> address compare
|
|
260
|
+
if (op === 'eq' && right.kind === 'literal_address') {
|
|
261
|
+
if (!actual) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
262
|
+
if (actual.type === 'other') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
263
|
+
return actual.type === 'address' && actual.value === right.value
|
|
264
|
+
? { permit: true }
|
|
265
|
+
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// eq(call_arg[i], literal_i128) -> BigInt compare
|
|
269
|
+
if (op === 'eq' && right.kind === 'literal_i128') {
|
|
270
|
+
if (actual?.type !== 'i128') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
271
|
+
return BigInt(actual.value) === BigInt(right.value)
|
|
272
|
+
? { permit: true }
|
|
273
|
+
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// eq(call_arg[i], literal_symbol)
|
|
277
|
+
if (op === 'eq' && right.kind === 'literal_symbol') {
|
|
278
|
+
if (!actual) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
279
|
+
return actual.type === 'symbol' && actual.value === right.value
|
|
280
|
+
? { permit: true }
|
|
281
|
+
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// eq(call_arg[i], literal_u32)
|
|
285
|
+
if (op === 'eq' && right.kind === 'literal_u32') {
|
|
286
|
+
if (!actual) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
287
|
+
return actual.type === 'u32' && actual.value === String(right.value)
|
|
288
|
+
? { permit: true }
|
|
289
|
+
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// eq(call_arg[i], literal_u64)
|
|
293
|
+
if (op === 'eq' && right.kind === 'literal_u64') {
|
|
294
|
+
if (!actual) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
295
|
+
return actual.type === 'u64' && actual.value === right.value
|
|
296
|
+
? { permit: true }
|
|
297
|
+
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// eq(call_arg[i], literal_bytes)
|
|
301
|
+
if (op === 'eq' && right.kind === 'literal_bytes') {
|
|
302
|
+
if (!actual) return { permit: false, reason: 'ARG_MISMATCH' }
|
|
303
|
+
return actual.type === 'bytes' && actual.value === right.value
|
|
304
|
+
? { permit: true }
|
|
305
|
+
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Anything else: fail closed on opacity (cannot decode the arg reliably).
|
|
309
|
+
if (!actual || actual.type === 'other') return { permit: false, reason: 'ARG_MISMATCH' }
|
|
310
|
+
return { permit: false, reason: 'ARG_MISMATCH' }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Ordered numeric comparison (lt/lte/gt/gte) on a `call_arg`. The interpreter
|
|
314
|
+
* reads the arg as an integer (i128 / u64 / u32 on the recorder's ScVal
|
|
315
|
+
* surface) and compares it to a numeric literal via BigInt. A non-numeric arg
|
|
316
|
+
* or a non-numeric literal fails closed (ARG_MISMATCH) rather than permitting
|
|
317
|
+
* an undecidable bound. Backs the SoroSwap input-amount cap
|
|
318
|
+
* (`call_arg[0] <= limit`). */
|
|
319
|
+
function evalArgOrderedCompare(
|
|
320
|
+
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
321
|
+
actual: ScVal | undefined,
|
|
322
|
+
right: PredicateLeaf
|
|
323
|
+
): EvalResult {
|
|
324
|
+
const actualInt = argNumericBigInt(actual)
|
|
325
|
+
const literalInt = literalNumericBigInt(right)
|
|
326
|
+
if (actualInt === null || literalInt === null) {
|
|
327
|
+
return { permit: false, reason: 'ARG_MISMATCH' }
|
|
328
|
+
}
|
|
329
|
+
return bigintCmp(op, actualInt.toString(), literalInt.toString())
|
|
330
|
+
? { permit: true }
|
|
331
|
+
: { permit: false, reason: 'ARG_MISMATCH' }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** BigInt value of a numeric-integer ScVal arg (i128 / u64 / u32), or null when
|
|
335
|
+
* the arg is absent, opaque, or a non-numeric type. */
|
|
336
|
+
function argNumericBigInt(actual: ScVal | undefined): bigint | null {
|
|
337
|
+
if (!actual) return null
|
|
338
|
+
if (actual.type === 'i128' || actual.type === 'u64' || actual.type === 'u32') {
|
|
339
|
+
try {
|
|
340
|
+
return BigInt(actual.value)
|
|
341
|
+
} catch {
|
|
342
|
+
return null
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Element-by-element ordered comparison of an `ScVal[]` against a
|
|
349
|
+
* `PredicateLeaf[]`. Equal-length and equal at every index => permit.
|
|
350
|
+
* Length mismatch OR any element mismatch => deny ARG_MISMATCH. */
|
|
351
|
+
function compareVecExact(actual: ScVal[], expected: PredicateLeaf[], ctx: EvalContext): boolean {
|
|
352
|
+
if (actual.length !== expected.length) return false
|
|
353
|
+
for (let i = 0; i < expected.length; i++) {
|
|
354
|
+
const e = expected[i]
|
|
355
|
+
const a = actual[i]
|
|
356
|
+
if (!e || !a) return false
|
|
357
|
+
const r = evalArgEq('eq', a, e, ctx)
|
|
358
|
+
if (!r.permit) return false
|
|
359
|
+
}
|
|
360
|
+
return true
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Step 5: `in` membership. Empty haystack ALWAYS denies. Opaque needle
|
|
364
|
+
* fails closed. */
|
|
365
|
+
function evalIn(needle: PredicateLeaf, haystack: PredicateLeaf[], ctx: EvalContext): EvalResult {
|
|
366
|
+
if (haystack.length === 0) return { permit: false, reason: 'NOT_IN_ALLOWLIST' }
|
|
367
|
+
// Resolve the needle's ScVal against the candidate call.
|
|
368
|
+
const actual = resolveLeaf(needle, ctx)
|
|
369
|
+
if (!actual || actual.type === 'other') return { permit: false, reason: 'NOT_IN_ALLOWLIST' }
|
|
370
|
+
for (const h of haystack) {
|
|
371
|
+
const r = evalArgEq('eq', actual, h, ctx)
|
|
372
|
+
if (r.permit) return { permit: true }
|
|
373
|
+
}
|
|
374
|
+
return { permit: false, reason: 'NOT_IN_ALLOWLIST' }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Resolve a selector leaf to its current ScVal against the candidate call. */
|
|
378
|
+
function resolveLeaf(leaf: PredicateLeaf, ctx: EvalContext): ScVal | undefined {
|
|
379
|
+
switch (leaf.kind) {
|
|
380
|
+
case 'call_contract':
|
|
381
|
+
return { type: 'address', value: ctx.contract }
|
|
382
|
+
case 'call_fn':
|
|
383
|
+
return { type: 'symbol', value: ctx.fn }
|
|
384
|
+
case 'call_arg':
|
|
385
|
+
return ctx.args[leaf.index]
|
|
386
|
+
case 'amount':
|
|
387
|
+
case 'window_spent':
|
|
388
|
+
case 'oracle_price':
|
|
389
|
+
case 'invocation_count_in_window':
|
|
390
|
+
case 'now':
|
|
391
|
+
case 'valid_until':
|
|
392
|
+
return undefined // selector leaves with no ScVal projection
|
|
393
|
+
case 'literal_address':
|
|
394
|
+
return { type: 'address', value: leaf.value }
|
|
395
|
+
case 'literal_i128':
|
|
396
|
+
return { type: 'i128', value: leaf.value }
|
|
397
|
+
case 'literal_symbol':
|
|
398
|
+
return { type: 'symbol', value: leaf.value }
|
|
399
|
+
case 'literal_u32':
|
|
400
|
+
return { type: 'u32', value: String(leaf.value) }
|
|
401
|
+
case 'literal_u64':
|
|
402
|
+
return { type: 'u64', value: leaf.value }
|
|
403
|
+
case 'literal_bytes':
|
|
404
|
+
return { type: 'bytes', value: leaf.value }
|
|
405
|
+
case 'literal_vec':
|
|
406
|
+
return undefined
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Step 6: amount compare on BigInt. */
|
|
411
|
+
function evalAmountCompare(
|
|
412
|
+
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
413
|
+
token: string,
|
|
414
|
+
right: PredicateLeaf,
|
|
415
|
+
ctx: EvalContext
|
|
416
|
+
): EvalResult {
|
|
417
|
+
const literal = right.kind === 'literal_i128' ? right.value : null
|
|
418
|
+
if (literal === null) return { permit: false, reason: 'AMOUNT_BOUND' }
|
|
419
|
+
const actual = ctx.amountByToken[token] ?? '0'
|
|
420
|
+
return bigintCmp(op, actual, literal)
|
|
421
|
+
? { permit: true }
|
|
422
|
+
: { permit: false, reason: 'AMOUNT_BOUND' }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Step 6: window_spent compare on BigInt. */
|
|
426
|
+
function evalWindowSpentCompare(
|
|
427
|
+
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
428
|
+
token: string,
|
|
429
|
+
right: PredicateLeaf,
|
|
430
|
+
ctx: EvalContext
|
|
431
|
+
): EvalResult {
|
|
432
|
+
const literal = right.kind === 'literal_i128' ? right.value : null
|
|
433
|
+
if (literal === null) return { permit: false, reason: 'AMOUNT_BOUND' }
|
|
434
|
+
const actual = ctx.windowSpentByToken[token] ?? '0'
|
|
435
|
+
return bigintCmp(op, actual, literal)
|
|
436
|
+
? { permit: true }
|
|
437
|
+
: { permit: false, reason: 'AMOUNT_BOUND' }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Step 7: invocation_count_in_window compare. */
|
|
441
|
+
function evalFrequencyCompare(
|
|
442
|
+
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
443
|
+
windowSecs: number,
|
|
444
|
+
right: PredicateLeaf,
|
|
445
|
+
ctx: EvalContext
|
|
446
|
+
): EvalResult {
|
|
447
|
+
const literal = right.kind === 'literal_u32' ? String(right.value) : null
|
|
448
|
+
if (literal === null) return { permit: false, reason: 'FREQUENCY' }
|
|
449
|
+
const actual = String(ctx.invocationCountByWindow[windowSecs] ?? 0)
|
|
450
|
+
return bigintCmp(op, actual, literal) ? { permit: true } : { permit: false, reason: 'FREQUENCY' }
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Step 8: oracle_price compare. Reads `ctx.oraclePriceByAsset[asset]`. Any
|
|
454
|
+
* error entry OR a missing key throws `OracleError` (FATAL). A satisfied
|
|
455
|
+
* compare permits. */
|
|
456
|
+
function evalOracleCompare(
|
|
457
|
+
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte',
|
|
458
|
+
asset: string,
|
|
459
|
+
right: PredicateLeaf,
|
|
460
|
+
ctx: EvalContext
|
|
461
|
+
): EvalResult {
|
|
462
|
+
const entry = ctx.oraclePriceByAsset[asset]
|
|
463
|
+
if (!entry) throw new OracleError('ORACLE_STALE')
|
|
464
|
+
if ('error' in entry) {
|
|
465
|
+
const mapped = ORACLE_ERROR_CODES[entry.error]
|
|
466
|
+
if (!mapped) throw new OracleError('ORACLE_STALE')
|
|
467
|
+
throw new OracleError(mapped)
|
|
468
|
+
}
|
|
469
|
+
const literal = right.kind === 'literal_i128' ? right.value : null
|
|
470
|
+
if (literal === null) throw new OracleError('ORACLE_DECIMALS_MISMATCH')
|
|
471
|
+
return bigintCmp(op, entry.price, literal)
|
|
472
|
+
? { permit: true }
|
|
473
|
+
: { permit: false, reason: 'FN_MISMATCH' }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** BigInt compare helper. `eq` is also supported by callers (selector-vs-literal
|
|
477
|
+
* equal checks), but the dedicated arg path uses it via `BigInt(actual) === BigInt(right)`. */
|
|
478
|
+
function bigintCmp(op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte', aStr: string, bStr: string): boolean {
|
|
479
|
+
const a = BigInt(aStr)
|
|
480
|
+
const b = BigInt(bStr)
|
|
481
|
+
switch (op) {
|
|
482
|
+
case 'eq':
|
|
483
|
+
return a === b
|
|
484
|
+
case 'lt':
|
|
485
|
+
return a < b
|
|
486
|
+
case 'lte':
|
|
487
|
+
return a <= b
|
|
488
|
+
case 'gt':
|
|
489
|
+
return a > b
|
|
490
|
+
case 'gte':
|
|
491
|
+
return a >= b
|
|
492
|
+
}
|
|
493
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { PredicateNode } from '../types.ts'
|
|
2
|
+
import type { GeneratedCases } from './deny-cases.ts'
|
|
3
|
+
import { evaluate } from './evaluate.ts'
|
|
4
|
+
|
|
5
|
+
export interface HarnessFailure {
|
|
6
|
+
dimension: string
|
|
7
|
+
expected: 'permit' | 'deny'
|
|
8
|
+
got: 'permit' | 'deny'
|
|
9
|
+
reason?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type HarnessResult = { ok: true } | { ok: false; failures: HarnessFailure[] }
|
|
13
|
+
|
|
14
|
+
/** Evaluate the intended call and every generated denial without side effects. */
|
|
15
|
+
export function runHarness(predicate: PredicateNode, cases: GeneratedCases): HarnessResult {
|
|
16
|
+
const failures: HarnessFailure[] = []
|
|
17
|
+
const permitResult = evaluate(predicate, cases.permit)
|
|
18
|
+
if (!permitResult.permit) {
|
|
19
|
+
failures.push({
|
|
20
|
+
dimension: 'PERMIT_CASE_FAILED',
|
|
21
|
+
expected: 'permit',
|
|
22
|
+
got: 'deny',
|
|
23
|
+
reason: permitResult.reason,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const deny of cases.denies) {
|
|
28
|
+
const result = evaluate(predicate, deny.ctx)
|
|
29
|
+
if (result.permit) {
|
|
30
|
+
failures.push({
|
|
31
|
+
dimension: deny.dimension,
|
|
32
|
+
expected: 'deny',
|
|
33
|
+
got: 'permit',
|
|
34
|
+
reason: 'DENY_CASE_FAILURE',
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return failures.length === 0 ? { ok: true } : { ok: false, failures }
|
|
40
|
+
}
|
package/src/synth/index.ts
CHANGED
|
@@ -5,7 +5,19 @@ export {
|
|
|
5
5
|
type ComposeResult,
|
|
6
6
|
composeFromRecording,
|
|
7
7
|
} from './compose-from-recording.ts'
|
|
8
|
+
export {
|
|
9
|
+
type DenyCase,
|
|
10
|
+
type GeneratedCases,
|
|
11
|
+
generateCases,
|
|
12
|
+
} from './deny-cases.ts'
|
|
13
|
+
export { type EvalContext, type EvalResult, evaluate } from './evaluate.ts'
|
|
14
|
+
export {
|
|
15
|
+
type HarnessFailure,
|
|
16
|
+
type HarnessResult,
|
|
17
|
+
runHarness,
|
|
18
|
+
} from './harness.ts'
|
|
8
19
|
export { type IntentFacts, lower } from './lower.ts'
|
|
20
|
+
export { minimize } from './minimize.ts'
|
|
9
21
|
export {
|
|
10
22
|
type DecideScopeOptions,
|
|
11
23
|
decideScope,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { PredicateNode } from '../types.ts'
|
|
2
|
+
import { type DenyCase, generateCases } from './deny-cases.ts'
|
|
3
|
+
import type { EvalContext } from './evaluate.ts'
|
|
4
|
+
import { runHarness } from './harness.ts'
|
|
5
|
+
|
|
6
|
+
/** Remove top-level conjuncts only when the current and regenerated batteries still deny. */
|
|
7
|
+
export function minimize(predicate: PredicateNode, permitCtx: EvalContext): PredicateNode {
|
|
8
|
+
if (predicate.op !== 'and') return predicate
|
|
9
|
+
|
|
10
|
+
let children = [...predicate.children]
|
|
11
|
+
let index = 0
|
|
12
|
+
|
|
13
|
+
while (index < children.length) {
|
|
14
|
+
const current: PredicateNode = { op: 'and', children }
|
|
15
|
+
const currentCases = generateCases(current, permitCtx)
|
|
16
|
+
const candidateChildren = children.filter((_, childIndex) => childIndex !== index)
|
|
17
|
+
const candidate: PredicateNode = { op: 'and', children: candidateChildren }
|
|
18
|
+
const candidateCases = generateCases(candidate, permitCtx)
|
|
19
|
+
const verificationCases = {
|
|
20
|
+
permit: candidateCases.permit,
|
|
21
|
+
denies: mergeDenyCases(currentCases.denies, candidateCases.denies),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (runHarness(candidate, verificationCases).ok) {
|
|
25
|
+
children = candidateChildren
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
index += 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { op: 'and', children }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function mergeDenyCases(current: DenyCase[], candidate: DenyCase[]): DenyCase[] {
|
|
35
|
+
const merged: DenyCase[] = []
|
|
36
|
+
const seen = new Set<string>()
|
|
37
|
+
for (const deny of [...current, ...candidate]) {
|
|
38
|
+
const key = `${deny.dimension}:${JSON.stringify(deny.ctx)}`
|
|
39
|
+
if (seen.has(key)) continue
|
|
40
|
+
seen.add(key)
|
|
41
|
+
merged.push(deny)
|
|
42
|
+
}
|
|
43
|
+
return merged
|
|
44
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/synth/predicate-literals.ts - read scalar values out of predicate literal leaves.
|
|
2
|
+
//
|
|
3
|
+
// Small shared helpers for interpreting a `PredicateLeaf` literal. Kept out of
|
|
4
|
+
// `evaluate.ts` so the evaluator stays focused on the deny-order semantics; the
|
|
5
|
+
// deny-case generator uses the same reader so both sides agree on what a numeric
|
|
6
|
+
// literal means.
|
|
7
|
+
|
|
8
|
+
import type { PredicateLeaf } from '../types.ts'
|
|
9
|
+
|
|
10
|
+
/** BigInt value of a numeric-integer literal leaf (`literal_i128` / `literal_u64`
|
|
11
|
+
* / `literal_u32`), or null for a non-numeric literal (address / symbol / bytes
|
|
12
|
+
* / vec). Callers fail closed on null. */
|
|
13
|
+
export function literalNumericBigInt(leaf: PredicateLeaf): bigint | null {
|
|
14
|
+
switch (leaf.kind) {
|
|
15
|
+
case 'literal_i128':
|
|
16
|
+
case 'literal_u64':
|
|
17
|
+
try {
|
|
18
|
+
return BigInt(leaf.value)
|
|
19
|
+
} catch {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
case 'literal_u32':
|
|
23
|
+
return BigInt(leaf.value)
|
|
24
|
+
default:
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
}
|