@crediolabs/policy-synth 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/adapters/interpreter/adapter.d.ts +19 -25
- package/dist/adapters/interpreter/adapter.js +54 -487
- package/dist/adapters/interpreter/index.d.ts +1 -1
- package/dist/adapters/interpreter/index.js +1 -1
- package/dist/adapters/oz/adapter.js +1 -15
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/install/build-add-context-rule.d.ts +3 -19
- package/dist/install/build-add-context-rule.js +3 -98
- package/dist/install/build-install-policy.d.ts +4 -53
- package/dist/install/build-install-policy.js +44 -123
- package/dist/install/index.d.ts +0 -2
- package/dist/install/index.js +0 -7
- package/dist/ir/index.d.ts +1 -1
- package/dist/ir/types.d.ts +7 -76
- package/dist/ir/types.js +0 -2
- package/dist/predicate/decode.d.ts +1 -1
- package/dist/predicate/decode.js +2 -70
- package/dist/predicate/encode.js +45 -229
- package/dist/predicate/from-json.js +1 -42
- package/dist/record/decode.js +3 -8
- package/dist/review-card/builder.d.ts +4 -3
- package/dist/review-card/builder.js +18 -162
- package/dist/review-card/cross-check.js +2 -105
- package/dist/review-card/render-leaf.d.ts +4 -0
- package/dist/review-card/render-leaf.js +47 -0
- package/dist/run/index.d.ts +36 -139
- package/dist/run/index.js +125 -437
- package/dist/run/schemas.d.ts +168 -1929
- package/dist/run/schemas.js +40 -269
- package/dist/seams/types.d.ts +0 -2
- package/dist/simulate/deny-cases.d.ts +26 -0
- package/dist/simulate/deny-cases.js +282 -0
- package/dist/simulate/evaluate.d.ts +18 -0
- package/dist/simulate/evaluate.js +271 -0
- package/dist/simulate/index.d.ts +4 -0
- package/dist/simulate/index.js +8 -0
- package/dist/synth/compose-from-recording.d.ts +24 -51
- package/dist/synth/compose-from-recording.js +120 -282
- package/dist/synth/deny-cases.d.ts +5 -7
- package/dist/synth/deny-cases.js +23 -373
- package/dist/synth/evaluate.d.ts +0 -21
- package/dist/synth/evaluate.js +56 -336
- package/dist/synth/index.d.ts +0 -5
- package/dist/synth/index.js +0 -5
- package/dist/synth/permit-context.d.ts +2 -9
- package/dist/synth/permit-context.js +1 -82
- package/dist/synth/synthesize-from-recording.d.ts +1 -29
- package/dist/synth/synthesize-from-recording.js +41 -356
- package/dist/types.d.ts +8 -60
- package/dist/types.js +5 -2
- package/dist/verify/simulate.d.ts +1 -14
- package/dist/verify/simulate.js +2 -88
- package/dist/verify/verify.d.ts +1 -7
- package/dist/verify/verify.js +0 -8
- package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
- package/dist-cjs/adapters/interpreter/adapter.js +56 -489
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -1
- package/dist-cjs/adapters/interpreter/index.js +1 -2
- package/dist-cjs/adapters/oz/adapter.js +1 -15
- package/dist-cjs/errors.d.ts +1 -1
- package/dist-cjs/index.d.ts +1 -4
- package/dist-cjs/index.js +1 -4
- package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
- package/dist-cjs/install/build-add-context-rule.js +1 -97
- package/dist-cjs/install/build-install-policy.d.ts +4 -53
- package/dist-cjs/install/build-install-policy.js +44 -122
- package/dist-cjs/install/index.d.ts +0 -2
- package/dist-cjs/install/index.js +2 -23
- package/dist-cjs/ir/index.d.ts +1 -1
- package/dist-cjs/ir/types.d.ts +7 -76
- package/dist-cjs/ir/types.js +0 -2
- package/dist-cjs/predicate/decode.d.ts +1 -1
- package/dist-cjs/predicate/decode.js +2 -70
- package/dist-cjs/predicate/encode.js +45 -229
- package/dist-cjs/predicate/from-json.js +1 -42
- package/dist-cjs/record/decode.js +3 -8
- package/dist-cjs/review-card/builder.d.ts +4 -3
- package/dist-cjs/review-card/builder.js +23 -167
- package/dist-cjs/review-card/cross-check.js +7 -110
- package/dist-cjs/review-card/render-leaf.d.ts +4 -0
- package/dist-cjs/review-card/render-leaf.js +52 -0
- package/dist-cjs/run/index.d.ts +36 -139
- package/dist-cjs/run/index.js +125 -444
- package/dist-cjs/run/schemas.d.ts +168 -1929
- package/dist-cjs/run/schemas.js +41 -270
- package/dist-cjs/seams/types.d.ts +0 -2
- package/dist-cjs/simulate/deny-cases.d.ts +26 -0
- package/dist-cjs/simulate/deny-cases.js +286 -0
- package/dist-cjs/simulate/evaluate.d.ts +18 -0
- package/dist-cjs/simulate/evaluate.js +274 -0
- package/dist-cjs/simulate/index.d.ts +4 -0
- package/dist-cjs/simulate/index.js +13 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
- package/dist-cjs/synth/compose-from-recording.js +120 -282
- package/dist-cjs/synth/deny-cases.d.ts +5 -7
- package/dist-cjs/synth/deny-cases.js +23 -374
- package/dist-cjs/synth/evaluate.d.ts +0 -21
- package/dist-cjs/synth/evaluate.js +57 -337
- package/dist-cjs/synth/index.d.ts +0 -5
- package/dist-cjs/synth/index.js +1 -11
- package/dist-cjs/synth/permit-context.d.ts +2 -9
- package/dist-cjs/synth/permit-context.js +1 -82
- package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
- package/dist-cjs/synth/synthesize-from-recording.js +39 -354
- package/dist-cjs/types.d.ts +8 -60
- package/dist-cjs/types.js +6 -3
- package/dist-cjs/verify/simulate.d.ts +1 -14
- package/dist-cjs/verify/simulate.js +2 -88
- package/dist-cjs/verify/verify.d.ts +1 -7
- package/dist-cjs/verify/verify.js +0 -8
- package/package.json +4 -4
- package/src/adapters/interpreter/adapter.ts +76 -572
- package/src/errors.ts +0 -19
- package/src/index.ts +1 -4
- package/src/install/build-add-context-rule.ts +5 -139
- package/src/install/build-install-policy.ts +93 -214
- package/src/install/index.ts +0 -34
- package/src/predicate/decode.ts +2 -70
- package/src/predicate/encode.ts +49 -261
- package/src/predicate/from-json.ts +1 -43
- package/src/record/decode.ts +3 -8
- package/src/review-card/builder.ts +19 -168
- package/src/review-card/cross-check.ts +3 -105
- package/src/review-card/render-leaf.ts +48 -0
- package/src/run/index.ts +144 -572
- package/src/run/schemas.ts +42 -291
- package/src/simulate/deny-cases.ts +334 -0
- package/src/simulate/evaluate.ts +284 -0
- package/src/simulate/index.ts +11 -0
- package/src/synth/compose-from-recording.ts +148 -347
- package/src/synth/index.ts +0 -13
- package/src/synth/synthesize-from-recording.ts +43 -456
- package/src/types.ts +13 -49
- package/dist/install/authority-overlap.d.ts +0 -134
- package/dist/install/authority-overlap.js +0 -0
- package/dist/install/build-install-predicate.d.ts +0 -96
- package/dist/install/build-install-predicate.js +0 -444
- package/dist/install/build-merge-policy.d.ts +0 -70
- package/dist/install/build-merge-policy.js +0 -130
- package/dist/install/plan-merge-policy.d.ts +0 -49
- package/dist/install/plan-merge-policy.js +0 -86
- package/dist/install/read-account-rules.d.ts +0 -100
- package/dist/install/read-account-rules.js +0 -283
- package/dist-cjs/install/authority-overlap.d.ts +0 -134
- package/dist-cjs/install/authority-overlap.js +0 -0
- package/dist-cjs/install/build-install-predicate.d.ts +0 -96
- package/dist-cjs/install/build-install-predicate.js +0 -479
- package/dist-cjs/install/build-merge-policy.d.ts +0 -70
- package/dist-cjs/install/build-merge-policy.js +0 -134
- package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
- package/dist-cjs/install/plan-merge-policy.js +0 -90
- package/dist-cjs/install/read-account-rules.d.ts +0 -100
- package/dist-cjs/install/read-account-rules.js +0 -296
- package/src/adapters/interpreter/index.ts +0 -8
- package/src/adapters/oz/adapter.ts +0 -376
- package/src/adapters/oz/index.ts +0 -9
- package/src/codegen/compile-gate.ts +0 -167
- package/src/codegen/index.ts +0 -17
- package/src/codegen/template.ts +0 -165
- package/src/install/authority-overlap.ts +0 -0
- package/src/install/build-merge-policy.ts +0 -219
- package/src/install/plan-merge-policy.ts +0 -133
- package/src/install/read-account-rules.ts +0 -376
- package/src/ir/index.ts +0 -13
- package/src/ir/types.ts +0 -132
- package/src/mandate/index.ts +0 -4
- package/src/mandate/to-ir.ts +0 -71
- package/src/mandate/types.ts +0 -21
- package/src/seams/index.ts +0 -11
- package/src/seams/types.ts +0 -81
- package/src/synth/deny-cases.ts +0 -663
- package/src/synth/evaluate.ts +0 -613
- package/src/synth/harness.ts +0 -68
- package/src/synth/minimize.ts +0 -48
- package/src/synth/permit-context.ts +0 -136
- package/src/synth/predicate-literals.ts +0 -27
- package/src/synth/synthesize-from-mandate.ts +0 -82
- package/src/verify/envelope.ts +0 -28
- package/src/verify/index.ts +0 -5
- package/src/verify/simulate.ts +0 -311
- package/src/verify/verify.ts +0 -243
|
@@ -1,46 +1,37 @@
|
|
|
1
1
|
// src/synth/synthesize-from-recording.ts - recording-path orchestrator.
|
|
2
2
|
//
|
|
3
3
|
// `synthesizeFromRecording` INFERS a bounded policy from a `RecordedTransaction`
|
|
4
|
-
// via the
|
|
4
|
+
// via the `PolicyIR` + interpreter adapter pair.
|
|
5
5
|
// Flow: validate -> parseConfidence gate -> lower -> decideScope -> composeFromRecording
|
|
6
|
-
// ->
|
|
7
|
-
// Same (tx, opts
|
|
6
|
+
// -> interpreter compile + self-verify.
|
|
7
|
+
// Same (tx, opts) -> byte-identical ProposedPolicy (no randomness, clock, globals).
|
|
8
8
|
|
|
9
9
|
import type { InterpreterAdapterConfig } from '../adapters/interpreter/adapter.ts'
|
|
10
10
|
import {
|
|
11
|
-
|
|
11
|
+
compileInterpreterPolicy,
|
|
12
12
|
lowerRuleToPredicate,
|
|
13
13
|
PLACEHOLDER_INTERPRETER_ADDRESS,
|
|
14
14
|
} from '../adapters/interpreter/adapter.ts'
|
|
15
|
-
import type { OzAdapterConfig } from '../adapters/oz/adapter.ts'
|
|
16
|
-
import { createOzAdapter } from '../adapters/oz/adapter.ts'
|
|
17
15
|
import type { ToolError, ToolResponse } from '../errors.ts'
|
|
18
16
|
import { encodePredicate } from '../predicate/encode.ts'
|
|
19
17
|
import {
|
|
20
|
-
|
|
21
|
-
MAX_SCVAL_CLONE_DEPTH,
|
|
18
|
+
GRAMMAR_VERSION,
|
|
22
19
|
type Network,
|
|
23
20
|
OZ_LIMITS,
|
|
24
|
-
type PredicateLeaf,
|
|
25
21
|
type PredicateNode,
|
|
26
22
|
type ProposedPolicy,
|
|
27
23
|
type RecordedTransaction,
|
|
28
24
|
SOROBAN_LIMITS,
|
|
29
25
|
} from '../types.ts'
|
|
30
|
-
import type { SimulationResult } from '../verify/envelope.ts'
|
|
31
26
|
import {
|
|
32
27
|
type ComposeOptions,
|
|
33
28
|
type ComposeUserResponses,
|
|
34
29
|
composeFromRecording,
|
|
35
30
|
} from './compose-from-recording.ts'
|
|
36
|
-
import { generateCases, ORIGINAL_DIMENSIONS } from './deny-cases.ts'
|
|
37
|
-
import { type EvalContext, evaluate } from './evaluate.ts'
|
|
38
|
-
import { runHarness } from './harness.ts'
|
|
39
31
|
import { lower } from './lower.ts'
|
|
40
|
-
import { minimize } from './minimize.ts'
|
|
41
32
|
import { decideScope, type ScopeDecision } from './scope.ts'
|
|
42
33
|
|
|
43
|
-
const UNCOVERED_PREFIX = 'Not
|
|
34
|
+
const UNCOVERED_PREFIX = 'Not expressible as a predicate constraint: '
|
|
44
35
|
|
|
45
36
|
/** Per-policy interpreter adapter config the caller opts into. Absent ->
|
|
46
37
|
* recording path runs in week-1 mode (OZ adapter only, warnings for the
|
|
@@ -52,10 +43,6 @@ export interface InterpreterAdapterOptions {
|
|
|
52
43
|
smartAccountAddress: string
|
|
53
44
|
/** Per-rule install nonce (first install = 1). Defaults to 1. */
|
|
54
45
|
installNonce?: number
|
|
55
|
-
/** Per-policy oracle overrides. Tighten-only vs the wasm defaults
|
|
56
|
-
* (maxStalenessSeconds <= 600, maxDeviationBps <= 200); widening is
|
|
57
|
-
* rejected at the options boundary. */
|
|
58
|
-
oracleParams?: { maxStalenessSeconds?: number; maxDeviationBps?: number }
|
|
59
46
|
}
|
|
60
47
|
|
|
61
48
|
/** PRIVATE test-only extension of `InterpreterAdapterOptions` for the
|
|
@@ -82,8 +69,7 @@ export interface SynthesizeFromRecordingOptions {
|
|
|
82
69
|
|
|
83
70
|
export function synthesizeFromRecording(
|
|
84
71
|
tx: RecordedTransaction,
|
|
85
|
-
opts: SynthesizeFromRecordingOptions
|
|
86
|
-
ozConfig: OzAdapterConfig
|
|
72
|
+
opts: SynthesizeFromRecordingOptions
|
|
87
73
|
): ToolResponse<ProposedPolicy> & {
|
|
88
74
|
/** Present iff `opts.explain === true` and the synthesis succeeded. The
|
|
89
75
|
* `predicateTree` is the exact in-memory `PredicateNode` (canonical
|
|
@@ -97,16 +83,15 @@ export function synthesizeFromRecording(
|
|
|
97
83
|
* builds a minimal honest SimulationResult downstream. */
|
|
98
84
|
explain?: {
|
|
99
85
|
predicateTree: PredicateNode | null
|
|
100
|
-
simulation: SimulationResult
|
|
101
86
|
}
|
|
102
87
|
} {
|
|
103
88
|
// Convert any ToolError-shaped throw (object with a string `.code`) to a
|
|
104
89
|
// structured `{ok:false, error}`; anything else is rethrown so genuine bugs
|
|
105
90
|
// crash instead of being silently swallowed. Wraps the entire body so
|
|
106
|
-
// cap errors (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) and
|
|
91
|
+
// cap errors (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) and encoder depth
|
|
107
92
|
// throws surface as structured ToolErrors rather than RangeErrors.
|
|
108
93
|
try {
|
|
109
|
-
return synthesizeFromRecordingInner(tx, opts
|
|
94
|
+
return synthesizeFromRecordingInner(tx, opts)
|
|
110
95
|
} catch (e) {
|
|
111
96
|
if (isToolErrorShape(e)) {
|
|
112
97
|
return {
|
|
@@ -145,12 +130,10 @@ function throwToolError(code: ToolError['code'], message: string): never {
|
|
|
145
130
|
|
|
146
131
|
function synthesizeFromRecordingInner(
|
|
147
132
|
tx: RecordedTransaction,
|
|
148
|
-
opts: SynthesizeFromRecordingOptions
|
|
149
|
-
ozConfig: OzAdapterConfig
|
|
133
|
+
opts: SynthesizeFromRecordingOptions
|
|
150
134
|
): ToolResponse<ProposedPolicy> & {
|
|
151
135
|
explain?: {
|
|
152
136
|
predicateTree: PredicateNode | null
|
|
153
|
-
simulation: SimulationResult
|
|
154
137
|
}
|
|
155
138
|
} {
|
|
156
139
|
// 0. validate inputs (fail closed - never synthesize from garbage).
|
|
@@ -250,7 +233,6 @@ function synthesizeFromRecordingInner(
|
|
|
250
233
|
const topLevel = tx.invocations[0] ?? null
|
|
251
234
|
const composeOpts: ComposeOptions = {
|
|
252
235
|
network: opts.network,
|
|
253
|
-
interpreterEnabled: opts.interpreter !== undefined,
|
|
254
236
|
...(opts.userResponses !== undefined ? { userResponses: opts.userResponses } : {}),
|
|
255
237
|
}
|
|
256
238
|
const composed = composeFromRecording(facts, scope.contract, topLevel, composeOpts)
|
|
@@ -259,24 +241,6 @@ function synthesizeFromRecordingInner(
|
|
|
259
241
|
// self-verify verdict (built from the SAME runHarness + evaluate that gated
|
|
260
242
|
// the synthesis, not a parallel simulation).
|
|
261
243
|
let explain: PredicateNode | null = null
|
|
262
|
-
let explainSim: SimulationResult | null = null
|
|
263
|
-
|
|
264
|
-
// 5. OZ compile (always runs).
|
|
265
|
-
const ozAdapter = createOzAdapter(ozConfig)
|
|
266
|
-
const compileRes = ozAdapter.compile(composed.ir)
|
|
267
|
-
|
|
268
|
-
if (!compileRes.proposed) {
|
|
269
|
-
return {
|
|
270
|
-
ok: false,
|
|
271
|
-
error: {
|
|
272
|
-
code: 'SYNTHESIS_ERROR',
|
|
273
|
-
message: `recording lowered to no installable OZ policy: ${compileRes.uncovered.join('; ')}`,
|
|
274
|
-
severity: 'error',
|
|
275
|
-
retryable: false,
|
|
276
|
-
details: { uncovered: compileRes.uncovered },
|
|
277
|
-
},
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
244
|
|
|
281
245
|
// 6. Interpreter compile (opt-in; fail-closed when the user routed real
|
|
282
246
|
// restrictions to the interpreter).
|
|
@@ -284,17 +248,13 @@ function synthesizeFromRecordingInner(
|
|
|
284
248
|
|
|
285
249
|
let interpreterPolicyDocument: ProposedPolicy['policyDocuments'][number] | null = null
|
|
286
250
|
let interpreterPolicyRef: ProposedPolicy['policyRefs'][number] | null = null
|
|
287
|
-
|
|
288
|
-
// warnings folded into `proposed.warnings[]` (which lives after that block)
|
|
289
|
-
// can read it. The block assigns it; the default is empty.
|
|
290
|
-
let permitCtxWarnings: string[] = []
|
|
251
|
+
let interpreterContextRule: ProposedPolicy['contextRule'] | null = null
|
|
291
252
|
|
|
292
253
|
if (interpreterOpts) {
|
|
293
254
|
const interpreterConfig: InterpreterAdapterConfig = {
|
|
294
255
|
network: opts.network,
|
|
295
256
|
installNonce: interpreterOpts.installNonce ?? 1,
|
|
296
257
|
smartAccountAddress: interpreterOpts.smartAccountAddress,
|
|
297
|
-
...(interpreterOpts.oracleParams ? { oracleParams: interpreterOpts.oracleParams } : {}),
|
|
298
258
|
}
|
|
299
259
|
|
|
300
260
|
let startingPredicate: PredicateNode | null = null
|
|
@@ -313,23 +273,18 @@ function synthesizeFromRecordingInner(
|
|
|
313
273
|
}
|
|
314
274
|
startingPredicate = testSeam
|
|
315
275
|
} else {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
let interpreterRes: Awaited<ReturnType<typeof interpreterAdapter.compile>>
|
|
276
|
+
let interpreterRes: ReturnType<typeof compileInterpreterPolicy>
|
|
319
277
|
try {
|
|
320
|
-
interpreterRes =
|
|
278
|
+
interpreterRes = compileInterpreterPolicy(composed.interpreterRule, interpreterConfig)
|
|
321
279
|
} catch (e) {
|
|
322
280
|
const code = (e as { code?: ToolError['code'] }).code
|
|
323
281
|
const allowedCodes: ToolError['code'][] = [
|
|
324
282
|
'SCOPE_SELF_CALL',
|
|
325
|
-
'ORACLE_LEAF_INVALID_POSITION',
|
|
326
|
-
'ORACLE_PARAMS_OUT_OF_RANGE',
|
|
327
283
|
'MALFORMED_PREDICATE',
|
|
328
284
|
'PREDICATE_TOO_LARGE',
|
|
329
285
|
'PREDICATE_TOO_DEEP',
|
|
330
286
|
'TOO_MANY_LEAVES',
|
|
331
287
|
'IN_OPERAND_LIMIT',
|
|
332
|
-
'PREDICATE_ORACLE_OVER_LIMIT',
|
|
333
288
|
]
|
|
334
289
|
const surfacedCode = code && allowedCodes.includes(code) ? code : 'SYNTHESIS_ERROR'
|
|
335
290
|
return {
|
|
@@ -370,6 +325,7 @@ function synthesizeFromRecordingInner(
|
|
|
370
325
|
|
|
371
326
|
interpreterPolicyDocument = interpreterRes.proposed.policyDocuments[0] ?? null
|
|
372
327
|
interpreterPolicyRef = interpreterRes.proposed.policyRefs[0] ?? null
|
|
328
|
+
interpreterContextRule = interpreterRes.proposed.contextRule
|
|
373
329
|
if (!interpreterPolicyDocument || !interpreterPolicyRef) {
|
|
374
330
|
return {
|
|
375
331
|
ok: false,
|
|
@@ -382,7 +338,7 @@ function synthesizeFromRecordingInner(
|
|
|
382
338
|
}
|
|
383
339
|
}
|
|
384
340
|
|
|
385
|
-
const firstInterpreterRule = composed.
|
|
341
|
+
const firstInterpreterRule = composed.interpreterRule
|
|
386
342
|
if (!firstInterpreterRule) {
|
|
387
343
|
return {
|
|
388
344
|
ok: false,
|
|
@@ -409,7 +365,9 @@ function synthesizeFromRecordingInner(
|
|
|
409
365
|
}
|
|
410
366
|
}
|
|
411
367
|
|
|
412
|
-
// 6b.
|
|
368
|
+
// 6b. The predicate is used as compiled. There is no minimisation pass:
|
|
369
|
+
// nothing strips a leaf and re-checks that the reduced predicate still
|
|
370
|
+
// denies, so the result is NOT a proven-minimal policy.
|
|
413
371
|
if (!topLevel) {
|
|
414
372
|
return {
|
|
415
373
|
ok: false,
|
|
@@ -432,93 +390,24 @@ function synthesizeFromRecordingInner(
|
|
|
432
390
|
},
|
|
433
391
|
}
|
|
434
392
|
}
|
|
435
|
-
|
|
436
|
-
// only the oracle-on-right normaliser) and folded into the proposed
|
|
437
|
-
// policy's `warnings[]` so the caller sees them on the success envelope.
|
|
438
|
-
const permitCtxResult = buildPermitContext(
|
|
439
|
-
tx,
|
|
440
|
-
scope,
|
|
441
|
-
topLevel,
|
|
442
|
-
opts.userResponses,
|
|
443
|
-
startingPredicate
|
|
444
|
-
)
|
|
445
|
-
const permitCtx = permitCtxResult.ctx
|
|
446
|
-
permitCtxWarnings = permitCtxResult.warnings
|
|
447
|
-
|
|
448
|
-
const finalPredicate: PredicateNode =
|
|
449
|
-
startingPredicate.op === 'and'
|
|
450
|
-
? minimize(startingPredicate, permitCtx, ORIGINAL_DIMENSIONS)
|
|
451
|
-
: startingPredicate
|
|
452
|
-
|
|
453
|
-
const harnessCases = generateCases(finalPredicate, permitCtx, ORIGINAL_DIMENSIONS)
|
|
454
|
-
const harnessResult = runHarness(finalPredicate, harnessCases)
|
|
455
|
-
if (!harnessResult.ok) {
|
|
456
|
-
return {
|
|
457
|
-
ok: false,
|
|
458
|
-
error: {
|
|
459
|
-
code: 'DENY_CASE_FAILURE',
|
|
460
|
-
message: `self-verify harness failed for the interpreter predicate (${harnessResult.failures.length} failure(s))`,
|
|
461
|
-
severity: 'error',
|
|
462
|
-
retryable: false,
|
|
463
|
-
details: { failures: harnessResult.failures },
|
|
464
|
-
},
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
const evalResult = evaluate(finalPredicate, permitCtx)
|
|
468
|
-
if (!evalResult.permit) {
|
|
469
|
-
return {
|
|
470
|
-
ok: false,
|
|
471
|
-
error: {
|
|
472
|
-
code: 'DENY_CASE_FAILURE',
|
|
473
|
-
message: `intended recorded call was denied by the interpreter predicate: ${evalResult.reason}`,
|
|
474
|
-
severity: 'error',
|
|
475
|
-
retryable: false,
|
|
476
|
-
details: { reason: evalResult.reason },
|
|
477
|
-
},
|
|
478
|
-
}
|
|
479
|
-
}
|
|
393
|
+
const finalPredicate: PredicateNode = startingPredicate
|
|
480
394
|
|
|
481
|
-
// --explain capture: quote the SAME verdict that gated the synthesis.
|
|
482
|
-
// Re-evaluate each deny case to surface its concrete reason (the harness
|
|
483
|
-
// only records whether the got-matches-expected boundary held).
|
|
484
395
|
if (opts.explain) {
|
|
485
396
|
explain = finalPredicate
|
|
486
|
-
const evaluatedCases: SimulationResult['evaluatedCases'] = [
|
|
487
|
-
{
|
|
488
|
-
dimension: 'permit',
|
|
489
|
-
outcome: evalResult.permit ? 'permit' : 'deny',
|
|
490
|
-
reason: 'matches recorded call',
|
|
491
|
-
},
|
|
492
|
-
]
|
|
493
|
-
for (const deny of harnessCases.denies) {
|
|
494
|
-
const r = evaluate(finalPredicate, deny.ctx)
|
|
495
|
-
evaluatedCases.push({
|
|
496
|
-
dimension: deny.dimension,
|
|
497
|
-
outcome: r.permit ? 'permit' : 'deny',
|
|
498
|
-
reason: r.permit ? 'no matching deny' : r.reason,
|
|
499
|
-
})
|
|
500
|
-
}
|
|
501
|
-
explainSim = {
|
|
502
|
-
permit: { tx: 'permit' },
|
|
503
|
-
evaluatedCases,
|
|
504
|
-
backend: 'ts-model',
|
|
505
|
-
simulatorVersion: 'ts-model-1.0.0',
|
|
506
|
-
}
|
|
507
397
|
}
|
|
508
398
|
|
|
509
|
-
// 6c.
|
|
510
|
-
//
|
|
399
|
+
// 6c. Encode the PredicateNode and stamp the canonical bytes back onto
|
|
400
|
+
// the PolicyDocument + PolicyRef. Cap breaches
|
|
511
401
|
// (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) throw ToolError-shaped errors;
|
|
512
402
|
// the outer envelope converts them to structured `{ok:false, error}`.
|
|
513
403
|
const { encodedPredicate, predicateHash } = encodePredicate(finalPredicate)
|
|
514
404
|
|
|
515
405
|
if (testSeam !== undefined) {
|
|
516
406
|
interpreterPolicyDocument = {
|
|
517
|
-
grammarVersion:
|
|
407
|
+
grammarVersion: GRAMMAR_VERSION,
|
|
518
408
|
installNonce: interpreterOpts.installNonce ?? 1,
|
|
519
409
|
encodedPredicate,
|
|
520
410
|
predicateHash,
|
|
521
|
-
...(interpreterOpts.oracleParams ? { oracleParams: interpreterOpts.oracleParams } : {}),
|
|
522
411
|
}
|
|
523
412
|
interpreterPolicyRef = {
|
|
524
413
|
kind: 'interpreter',
|
|
@@ -551,19 +440,25 @@ function synthesizeFromRecordingInner(
|
|
|
551
440
|
}
|
|
552
441
|
}
|
|
553
442
|
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
443
|
+
// 7. Assemble the ProposedPolicy. The interpreter is the only backend, so
|
|
444
|
+
// its context rule and policy ref are the whole policy. Without interpreter
|
|
445
|
+
// options nothing installable was produced, which is an error rather than
|
|
446
|
+
// an empty-but-successful policy.
|
|
447
|
+
if (!interpreterContextRule || !interpreterPolicyRef) {
|
|
448
|
+
return {
|
|
449
|
+
ok: false,
|
|
450
|
+
error: {
|
|
451
|
+
code: 'SYNTHESIS_ERROR',
|
|
452
|
+
message:
|
|
453
|
+
'no installable policy was synthesised: supply `interpreter` options (a smart account address) so the recording can be lowered to an interpreter predicate',
|
|
454
|
+
severity: 'error',
|
|
455
|
+
retryable: false,
|
|
456
|
+
details: { uncovered: composed.warnings },
|
|
457
|
+
},
|
|
458
|
+
}
|
|
459
|
+
}
|
|
561
460
|
|
|
562
|
-
|
|
563
|
-
const ozRefs = compileRes.proposed.policyRefs
|
|
564
|
-
const mergedRefs: ProposedPolicy['policyRefs'] = []
|
|
565
|
-
if (interpreterPolicyRef) mergedRefs.push(interpreterPolicyRef)
|
|
566
|
-
for (const r of ozRefs) mergedRefs.push(r)
|
|
461
|
+
const mergedRefs: ProposedPolicy['policyRefs'] = [interpreterPolicyRef]
|
|
567
462
|
|
|
568
463
|
if (mergedRefs.length > OZ_LIMITS.maxPoliciesPerRule) {
|
|
569
464
|
return {
|
|
@@ -578,7 +473,7 @@ function synthesizeFromRecordingInner(
|
|
|
578
473
|
}
|
|
579
474
|
|
|
580
475
|
const mergedContextRule = {
|
|
581
|
-
...
|
|
476
|
+
...interpreterContextRule,
|
|
582
477
|
policies: mergedRefs,
|
|
583
478
|
}
|
|
584
479
|
|
|
@@ -598,12 +493,7 @@ function synthesizeFromRecordingInner(
|
|
|
598
493
|
policyDocuments: interpreterPolicyDocument ? [interpreterPolicyDocument] : [],
|
|
599
494
|
policyRefs: mergedRefs,
|
|
600
495
|
parseConfidence: { ...tx.parseConfidence },
|
|
601
|
-
warnings: [
|
|
602
|
-
...zeroPolicyWarning,
|
|
603
|
-
...ozUncovered.map((u) => `${UNCOVERED_PREFIX}${u}`),
|
|
604
|
-
...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`),
|
|
605
|
-
...permitCtxWarnings,
|
|
606
|
-
],
|
|
496
|
+
warnings: [...zeroPolicyWarning, ...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`)],
|
|
607
497
|
ambiguities: mergeAmbiguities(composed.ambiguities, scope.ambiguities),
|
|
608
498
|
}
|
|
609
499
|
// --explain success envelope. When opts.explain is set and the OZ-only
|
|
@@ -613,48 +503,16 @@ function synthesizeFromRecordingInner(
|
|
|
613
503
|
const envelope: ToolResponse<ProposedPolicy> & {
|
|
614
504
|
explain?: {
|
|
615
505
|
predicateTree: PredicateNode | null
|
|
616
|
-
simulation: SimulationResult
|
|
617
506
|
}
|
|
618
507
|
} = { ok: true, data: proposed }
|
|
619
508
|
if (opts.explain) {
|
|
620
|
-
|
|
621
|
-
envelope.explain = { predicateTree: explain, simulation: explainSim }
|
|
622
|
-
} else {
|
|
623
|
-
envelope.explain = {
|
|
624
|
-
predicateTree: null,
|
|
625
|
-
simulation: {
|
|
626
|
-
permit: {
|
|
627
|
-
tx: 'deny',
|
|
628
|
-
reason: 'No self-verification was performed (interpreter adapter was not engaged)',
|
|
629
|
-
},
|
|
630
|
-
evaluatedCases: [],
|
|
631
|
-
backend: 'ts-model',
|
|
632
|
-
simulatorVersion: 'not-run',
|
|
633
|
-
},
|
|
634
|
-
}
|
|
635
|
-
}
|
|
509
|
+
envelope.explain = { predicateTree: explain }
|
|
636
510
|
}
|
|
637
511
|
return envelope
|
|
638
512
|
}
|
|
639
513
|
|
|
640
514
|
export { throwToolError }
|
|
641
515
|
|
|
642
|
-
/** OZ-side `uncovered` warning patterns the interpreter adapter actually lowers
|
|
643
|
-
* when wired in. When the interpreter adapter succeeds, matching entries are
|
|
644
|
-
* dropped from the OZ uncovered list so user-facing warnings reflect what is
|
|
645
|
-
* still UN-enforced rather than what OZ alone could not do. Matches the
|
|
646
|
-
* exact descriptor strings the OZ adapter emits (see `src/adapters/oz/adapter.ts`
|
|
647
|
-
* `describeCondition` / `describeSelector`). */
|
|
648
|
-
const INTERPRETER_COVERED_OZ_PATTERN =
|
|
649
|
-
/^per-method scoping to|^value allowlist on arg|^exact ordered sequence on arg|^oracle price condition on|^invocation-count window|^spending_limit on token .+ needs a CallContract context scoped to that token/
|
|
650
|
-
|
|
651
|
-
/** Reject non-sane inputs before any policy is synthesized. windowSeconds /
|
|
652
|
-
* validUntilLedger / invocationLimit must be positive integers; limitAmount a
|
|
653
|
-
* positive i128 decimal string; network mainnet|testnet. When the interpreter
|
|
654
|
-
* adapter is opted in, `smartAccountAddress` must be a C... contract address
|
|
655
|
-
* (the on-chain policy-bound account, NOT the G... source account),
|
|
656
|
-
* `installNonce` must fit u32 (default 1), and `oracleParams` must
|
|
657
|
-
* tighten-only vs the wasm defaults. */
|
|
658
516
|
function validateOptions(opts: SynthesizeFromRecordingOptions): ToolError | null {
|
|
659
517
|
if (opts.network !== 'mainnet' && opts.network !== 'testnet') {
|
|
660
518
|
return synthesisError(`network must be 'mainnet' or 'testnet', got: ${String(opts.network)}`)
|
|
@@ -669,9 +527,6 @@ function validateOptions(opts: SynthesizeFromRecordingOptions): ToolError | null
|
|
|
669
527
|
}
|
|
670
528
|
const ur = opts.userResponses
|
|
671
529
|
if (ur) {
|
|
672
|
-
if (ur.windowSeconds !== undefined && !isPositiveInt(ur.windowSeconds)) {
|
|
673
|
-
return synthesisError(`windowSeconds must be a positive integer, got: ${ur.windowSeconds}`)
|
|
674
|
-
}
|
|
675
530
|
if (
|
|
676
531
|
ur.validUntilLedger !== undefined &&
|
|
677
532
|
(!isPositiveInt(ur.validUntilLedger) || ur.validUntilLedger > SOROBAN_LIMITS.u32Max)
|
|
@@ -680,11 +535,6 @@ function validateOptions(opts: SynthesizeFromRecordingOptions): ToolError | null
|
|
|
680
535
|
`validUntilLedger must be a positive u32 ledger sequence (<= ${SOROBAN_LIMITS.u32Max}), got: ${ur.validUntilLedger}`
|
|
681
536
|
)
|
|
682
537
|
}
|
|
683
|
-
if (ur.invocationLimit !== undefined && !isPositiveInt(ur.invocationLimit)) {
|
|
684
|
-
return synthesisError(
|
|
685
|
-
`invocationLimit must be a positive integer, got: ${ur.invocationLimit}`
|
|
686
|
-
)
|
|
687
|
-
}
|
|
688
538
|
if (ur.limitAmount !== undefined && !isPositiveI128(ur.limitAmount)) {
|
|
689
539
|
return synthesisError(
|
|
690
540
|
`limitAmount must be a positive i128 decimal string within [1, ${I128_MAX}] (2^127-1), got: ${ur.limitAmount}`
|
|
@@ -718,27 +568,6 @@ function validateOptions(opts: SynthesizeFromRecordingOptions): ToolError | null
|
|
|
718
568
|
`interpreter.installNonce must be a positive u32 integer (<= ${SOROBAN_LIMITS.u32Max}), got: ${nonce}`
|
|
719
569
|
)
|
|
720
570
|
}
|
|
721
|
-
const op = opts.interpreter.oracleParams
|
|
722
|
-
if (op) {
|
|
723
|
-
const MAX_STALE = 600
|
|
724
|
-
const MAX_DEV = 200
|
|
725
|
-
if (
|
|
726
|
-
op.maxStalenessSeconds !== undefined &&
|
|
727
|
-
(!isPositiveInt(op.maxStalenessSeconds) || op.maxStalenessSeconds > MAX_STALE)
|
|
728
|
-
) {
|
|
729
|
-
return synthesisError(
|
|
730
|
-
`interpreter.oracleParams.maxStalenessSeconds must be a positive integer <= ${MAX_STALE} (tighten-only), got: ${op.maxStalenessSeconds}`
|
|
731
|
-
)
|
|
732
|
-
}
|
|
733
|
-
if (
|
|
734
|
-
op.maxDeviationBps !== undefined &&
|
|
735
|
-
(!isPositiveInt(op.maxDeviationBps) || op.maxDeviationBps > MAX_DEV)
|
|
736
|
-
) {
|
|
737
|
-
return synthesisError(
|
|
738
|
-
`interpreter.oracleParams.maxDeviationBps must be a positive integer <= ${MAX_DEV} (tighten-only), got: ${op.maxDeviationBps}`
|
|
739
|
-
)
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
571
|
}
|
|
743
572
|
return null
|
|
744
573
|
}
|
|
@@ -798,245 +627,3 @@ function mergeAmbiguities(
|
|
|
798
627
|
}
|
|
799
628
|
return out
|
|
800
629
|
}
|
|
801
|
-
|
|
802
|
-
/** Build the permit `EvalContext` the self-verify harness drives. Shape
|
|
803
|
-
* mirrors the intended recorded call so:
|
|
804
|
-
* - `evaluate(predicate, ctx).permit === true` must hold (a failure
|
|
805
|
-
* surfaces as DENY_CASE_FAILURE).
|
|
806
|
-
* - `generateCases(predicate, ctx)` produces a deny battery that reflects
|
|
807
|
-
* the actual recorded move (real amount, args, window start).
|
|
808
|
-
* Amounts are summed per-token over all movements (BigInt, never lossy).
|
|
809
|
-
* `oraclePriceByAsset` contains a price+timestamp satisfying each
|
|
810
|
-
* `oracle_price` leaf so the intended call permits under every bound;
|
|
811
|
-
* the harness mutates those entries (stale / missing / deviation / paused)
|
|
812
|
-
* to exercise the ORACLE_* deny paths. */
|
|
813
|
-
function buildPermitContext(
|
|
814
|
-
tx: RecordedTransaction,
|
|
815
|
-
scope: Extract<ScopeDecision, { kind: 'call_contract' }>,
|
|
816
|
-
topLevel: ContractInvocation,
|
|
817
|
-
userResponses: ComposeUserResponses | undefined,
|
|
818
|
-
predicate: PredicateNode
|
|
819
|
-
): { ctx: EvalContext; warnings: string[] } {
|
|
820
|
-
const amountByToken: Record<string, string> = {}
|
|
821
|
-
const totals = new Map<string, bigint>()
|
|
822
|
-
for (const m of tx.tokenMovements) {
|
|
823
|
-
const current = totals.get(m.token) ?? 0n
|
|
824
|
-
totals.set(m.token, current + BigInt(m.amount))
|
|
825
|
-
}
|
|
826
|
-
for (const [token, total] of totals) {
|
|
827
|
-
amountByToken[token] = total.toString()
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
const warnings: string[] = []
|
|
831
|
-
const oraclePriceByAsset: EvalContext['oraclePriceByAsset'] = oracleSatisfyingPrices(
|
|
832
|
-
predicate,
|
|
833
|
-
tx.fetchedAt,
|
|
834
|
-
warnings
|
|
835
|
-
)
|
|
836
|
-
|
|
837
|
-
const ctx: EvalContext = {
|
|
838
|
-
contract: scope.contract,
|
|
839
|
-
fn: topLevel.fn,
|
|
840
|
-
args: topLevel.args.map(cloneScVal),
|
|
841
|
-
atLedger: tx.ledgerSequence,
|
|
842
|
-
nowSeconds: tx.fetchedAt,
|
|
843
|
-
amountByToken,
|
|
844
|
-
windowSpentByToken: {},
|
|
845
|
-
invocationCountByWindow: {},
|
|
846
|
-
oraclePriceByAsset,
|
|
847
|
-
}
|
|
848
|
-
if (userResponses?.validUntilLedger !== undefined) {
|
|
849
|
-
ctx.validUntilLedger = userResponses.validUntilLedger
|
|
850
|
-
}
|
|
851
|
-
return { ctx, warnings }
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function cloneScVal(
|
|
855
|
-
value: { type: string; value: unknown },
|
|
856
|
-
depth = 0
|
|
857
|
-
): ContractInvocation['args'][number] {
|
|
858
|
-
// Clone top-level shells so the harness can mutate deny cases without
|
|
859
|
-
// aliasing the recorded call. Recursion bounded by MAX_SCVAL_CLONE_DEPTH so
|
|
860
|
-
// a hand-crafted nested-vec cannot RangeError the JS stack; the over-depth
|
|
861
|
-
// branch throws a ToolError-shaped error the envelope converts to
|
|
862
|
-
// `{ok:false, error}`.
|
|
863
|
-
if (value.type === 'vec') {
|
|
864
|
-
if (depth >= MAX_SCVAL_CLONE_DEPTH) {
|
|
865
|
-
throw cloneDepthError(value)
|
|
866
|
-
}
|
|
867
|
-
return {
|
|
868
|
-
type: 'vec',
|
|
869
|
-
value: (value.value as ContractInvocation['args'][number][]).map((v) =>
|
|
870
|
-
cloneScVal(v, depth + 1)
|
|
871
|
-
),
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
return { ...value } as ContractInvocation['args'][number]
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
function cloneDepthError(value: { type: string; value: unknown }): never {
|
|
878
|
-
const err = new Error(
|
|
879
|
-
`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`
|
|
880
|
-
) as Error & { code: string; severity: string; retryable: boolean; depthContext: unknown }
|
|
881
|
-
err.code = 'SYNTHESIS_ERROR'
|
|
882
|
-
err.severity = 'error'
|
|
883
|
-
err.retryable = false
|
|
884
|
-
err.depthContext = value.type
|
|
885
|
-
throw err
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
/** Walk every `oracle_price` leaf and return a price map whose entries satisfy
|
|
889
|
-
* the bound. Timestamp pinned to `nowSeconds` (the recorded `fetchedAt`) so
|
|
890
|
-
* the fresh-oracle deny case in `generateCases` is the only path that flips
|
|
891
|
-
* this map. Negatives clamped at 0 - oracle prices are non-negative on Stellar. */
|
|
892
|
-
function oracleSatisfyingPrices(
|
|
893
|
-
predicate: PredicateNode,
|
|
894
|
-
nowSeconds: number,
|
|
895
|
-
warnings: string[]
|
|
896
|
-
): EvalContext['oraclePriceByAsset'] {
|
|
897
|
-
const out: EvalContext['oraclePriceByAsset'] = {}
|
|
898
|
-
visitOracleLeaves(
|
|
899
|
-
predicate,
|
|
900
|
-
(asset, op, bound) => {
|
|
901
|
-
let price: bigint
|
|
902
|
-
switch (op) {
|
|
903
|
-
case 'lt':
|
|
904
|
-
case 'gt':
|
|
905
|
-
price = op === 'lt' ? bound - 1n : bound + 1n
|
|
906
|
-
break
|
|
907
|
-
case 'lte':
|
|
908
|
-
case 'gte':
|
|
909
|
-
case 'eq':
|
|
910
|
-
price = bound
|
|
911
|
-
break
|
|
912
|
-
}
|
|
913
|
-
if (price < 0n) price = 0n
|
|
914
|
-
out[asset] = { price: price.toString(), timestampSeconds: nowSeconds }
|
|
915
|
-
},
|
|
916
|
-
(warning) => {
|
|
917
|
-
// Cross-layer L3: oracle-on-right that cannot be normalised is
|
|
918
|
-
// surfaced as a warning, not silently dropped. The Rust interpreter
|
|
919
|
-
// would surface this as `UnsupportedNode`; the warning lets the
|
|
920
|
-
// caller decide whether to fix the predicate shape or accept the
|
|
921
|
-
// over-permissive hole.
|
|
922
|
-
warnings.push(warning.message)
|
|
923
|
-
}
|
|
924
|
-
)
|
|
925
|
-
return out
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
// Cross-layer L3: surface oracle-on-right as a WARNING rather than silently
|
|
929
|
-
// dropping it. The Rust interpreter only fires `eval_oracle_compare` when the
|
|
930
|
-
// oracle leaf is on the LEFT (`oracle_price op threshold`); a predicate that
|
|
931
|
-
// inverts the shape (`threshold op oracle_price`) falls through to
|
|
932
|
-
// `UnsupportedNode` instead. The TS model and the lowering path both attempt
|
|
933
|
-
// to normalise oracle-on-right by flipping the operator so the oracle ends
|
|
934
|
-
// up on the left, but the normalisation is only possible when the other
|
|
935
|
-
// side is a literal we can read as a bigint (an `oracle_threshold`, an
|
|
936
|
-
// `i128`/`u32`/`u64` literal). When it isn't (e.g. `oracle_price < call_arg[i]`),
|
|
937
|
-
// the case cannot be normalised - previously we silently dropped it, which
|
|
938
|
-
// means the harness would never exercise the bound and `minimize` could
|
|
939
|
-
// prune it. The warning is purely additive: the existing normalisation
|
|
940
|
-
// path is preserved, the case is just also reported so callers can decide
|
|
941
|
-
// to either fix the predicate shape or accept the over-permissive hole.
|
|
942
|
-
|
|
943
|
-
/** Diagnostic reported by `visitOracleLeaves` when an oracle comparison
|
|
944
|
-
* cannot be normalised to oracle-on-left. Currently only "oracle-on-right
|
|
945
|
-
* with a non-literal LHS" - the other branches already produce a parseable
|
|
946
|
-
* threshold and are returned to the caller. */
|
|
947
|
-
export interface OracleNormalisationWarning {
|
|
948
|
-
/** The dimension name (currently always "oracle_normalisation_dropped"). */
|
|
949
|
-
dimension: 'oracle_normalisation_dropped'
|
|
950
|
-
/** The operator as written. */
|
|
951
|
-
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte'
|
|
952
|
-
/** The asset the oracle leaf is bound to. */
|
|
953
|
-
asset: string
|
|
954
|
-
/** The non-literal RHS (oracle-on-left) or LHS (oracle-on-right) leaf
|
|
955
|
-
* whose shape stopped the normalisation. Surfaced for diagnostics;
|
|
956
|
-
* intentionally a partial view - the full leaf is the caller's job. */
|
|
957
|
-
otherKind: string
|
|
958
|
-
/** Human-readable message (matches the warn-on-drop text). */
|
|
959
|
-
message: string
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
function visitOracleLeaves(
|
|
963
|
-
node: PredicateNode,
|
|
964
|
-
visit: (asset: string, op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte', bound: bigint) => void,
|
|
965
|
-
onWarning?: (warning: OracleNormalisationWarning) => void
|
|
966
|
-
): void {
|
|
967
|
-
switch (node.op) {
|
|
968
|
-
case 'and':
|
|
969
|
-
case 'or':
|
|
970
|
-
for (const child of node.children) visitOracleLeaves(child, visit, onWarning)
|
|
971
|
-
return
|
|
972
|
-
case 'not':
|
|
973
|
-
visitOracleLeaves(node.child, visit, onWarning)
|
|
974
|
-
return
|
|
975
|
-
case 'eq':
|
|
976
|
-
case 'lt':
|
|
977
|
-
case 'lte':
|
|
978
|
-
case 'gt':
|
|
979
|
-
case 'gte': {
|
|
980
|
-
const leftLeaf: PredicateLeaf = node.left
|
|
981
|
-
const rightLeaf: PredicateLeaf = node.right
|
|
982
|
-
const leftIsOracle = leftLeaf.kind === 'oracle_price'
|
|
983
|
-
const rightIsOracle = rightLeaf.kind === 'oracle_price'
|
|
984
|
-
let oracleAsset: string | undefined
|
|
985
|
-
let literal: bigint | undefined
|
|
986
|
-
if (leftIsOracle) {
|
|
987
|
-
oracleAsset = leftLeaf.asset
|
|
988
|
-
literal = oracleLiteralFromLeaf(rightLeaf)
|
|
989
|
-
} else if (rightIsOracle) {
|
|
990
|
-
oracleAsset = rightLeaf.asset
|
|
991
|
-
literal = oracleLiteralFromLeaf(leftLeaf)
|
|
992
|
-
}
|
|
993
|
-
if (oracleAsset === undefined || literal === undefined) {
|
|
994
|
-
// Two reasons the case cannot be normalised:
|
|
995
|
-
// - neither side is an oracle leaf (visitor has nothing to do)
|
|
996
|
-
// - oracle-on-right with a non-literal LHS (Rust dispatch would
|
|
997
|
-
// hit UnsupportedNode; the TS model cannot build a permit ctx
|
|
998
|
-
// for it)
|
|
999
|
-
// Only the second is a meaningful warning; the first is a no-op
|
|
1000
|
-
// (the visitor was called for a non-oracle comparison). Emit the
|
|
1001
|
-
// warning when an oracle IS present on one side but the other
|
|
1002
|
-
// side is not a parseable threshold literal.
|
|
1003
|
-
if (rightIsOracle) {
|
|
1004
|
-
onWarning?.({
|
|
1005
|
-
dimension: 'oracle_normalisation_dropped',
|
|
1006
|
-
op: node.op,
|
|
1007
|
-
asset: rightLeaf.kind === 'oracle_price' ? rightLeaf.asset : '',
|
|
1008
|
-
otherKind: leftLeaf.kind,
|
|
1009
|
-
message: `oracle-on-right cannot be normalised: the LHS (kind=${leftLeaf.kind}) is not an oracle_threshold literal; the Rust interpreter would surface UnsupportedNode here`,
|
|
1010
|
-
})
|
|
1011
|
-
}
|
|
1012
|
-
return
|
|
1013
|
-
}
|
|
1014
|
-
visit(oracleAsset, node.op, literal)
|
|
1015
|
-
return
|
|
1016
|
-
}
|
|
1017
|
-
case 'in':
|
|
1018
|
-
// `in` is pure membership; oracle leaves inside haystacks are
|
|
1019
|
-
// forbidden by the position rule, so there's nothing to set up here.
|
|
1020
|
-
return
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
/** Restate an oracle threshold on the normalised 9-dp basis prices use.
|
|
1025
|
-
* Thresholds carry their own basis, so reading the digits raw would build
|
|
1026
|
-
* a permit context off by 10^(decimals-9) and the intended call would not
|
|
1027
|
-
* satisfy its own bound. Mirrors NORMALISED_DECIMALS in oracle.rs. */
|
|
1028
|
-
function oracleLiteralFromLeaf(leaf: PredicateLeaf): bigint | undefined {
|
|
1029
|
-
if (leaf.kind !== 'oracle_threshold') return undefined
|
|
1030
|
-
let value: bigint
|
|
1031
|
-
try {
|
|
1032
|
-
value = BigInt(leaf.value)
|
|
1033
|
-
} catch {
|
|
1034
|
-
return undefined
|
|
1035
|
-
}
|
|
1036
|
-
const normalised = 9n
|
|
1037
|
-
const decimals = BigInt(leaf.decimals)
|
|
1038
|
-
if (decimals <= normalised) return value * 10n ** (normalised - decimals)
|
|
1039
|
-
// Floor: a finer-grained threshold has no exact 9-dp representation; the
|
|
1040
|
-
// caller offsets by one from here to land on the right side of the bound.
|
|
1041
|
-
return value / 10n ** (decimals - normalised)
|
|
1042
|
-
}
|