@crediolabs/policy-synth 0.2.0 → 0.3.1
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/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/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/record/freshness.d.ts +17 -11
- package/dist/record/freshness.js +28 -18
- 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/{synth → simulate}/deny-cases.d.ts +4 -8
- 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/index.d.ts +0 -5
- package/dist/synth/index.js +0 -5
- 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-cjs/adapters/interpreter/adapter.d.ts +19 -25
- package/dist-cjs/adapters/interpreter/adapter.js +56 -489
- 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/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/record/freshness.d.ts +17 -11
- package/dist-cjs/record/freshness.js +28 -18
- 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/{synth → simulate}/deny-cases.d.ts +4 -8
- 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/index.d.ts +0 -5
- package/dist-cjs/synth/index.js +1 -11
- 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/package.json +5 -5
- 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/record/freshness.ts +29 -18
- 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/adapters/interpreter/index.d.ts +0 -1
- package/dist/adapters/interpreter/index.js +0 -2
- package/dist/adapters/oz/adapter.d.ts +0 -20
- package/dist/adapters/oz/adapter.js +0 -295
- package/dist/adapters/oz/index.d.ts +0 -1
- package/dist/adapters/oz/index.js +0 -2
- package/dist/codegen/compile-gate.d.ts +0 -33
- package/dist/codegen/compile-gate.js +0 -124
- package/dist/codegen/index.d.ts +0 -2
- package/dist/codegen/index.js +0 -8
- package/dist/codegen/template.d.ts +0 -18
- package/dist/codegen/template.js +0 -148
- 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/ir/index.d.ts +0 -1
- package/dist/ir/index.js +0 -2
- package/dist/ir/types.d.ts +0 -140
- package/dist/ir/types.js +0 -11
- package/dist/mandate/index.d.ts +0 -2
- package/dist/mandate/index.js +0 -2
- package/dist/mandate/to-ir.d.ts +0 -3
- package/dist/mandate/to-ir.js +0 -60
- package/dist/mandate/types.d.ts +0 -20
- package/dist/mandate/types.js +0 -8
- package/dist/seams/index.d.ts +0 -1
- package/dist/seams/index.js +0 -2
- package/dist/seams/types.d.ts +0 -66
- package/dist/seams/types.js +0 -11
- package/dist/synth/deny-cases.js +0 -562
- package/dist/synth/evaluate.d.ts +0 -39
- package/dist/synth/evaluate.js +0 -551
- package/dist/synth/harness.d.ts +0 -28
- package/dist/synth/harness.js +0 -47
- package/dist/synth/minimize.d.ts +0 -4
- package/dist/synth/minimize.js +0 -38
- package/dist/synth/permit-context.d.ts +0 -15
- package/dist/synth/permit-context.js +0 -116
- package/dist/synth/predicate-literals.d.ts +0 -5
- package/dist/synth/predicate-literals.js +0 -25
- package/dist/synth/synthesize-from-mandate.d.ts +0 -20
- package/dist/synth/synthesize-from-mandate.js +0 -59
- package/dist/verify/envelope.d.ts +0 -15
- package/dist/verify/envelope.js +0 -22
- package/dist/verify/index.d.ts +0 -3
- package/dist/verify/index.js +0 -3
- package/dist/verify/simulate.d.ts +0 -31
- package/dist/verify/simulate.js +0 -258
- package/dist/verify/verify.d.ts +0 -21
- package/dist/verify/verify.js +0 -189
- package/dist-cjs/adapters/interpreter/index.d.ts +0 -1
- package/dist-cjs/adapters/interpreter/index.js +0 -8
- package/dist-cjs/adapters/oz/adapter.d.ts +0 -20
- package/dist-cjs/adapters/oz/adapter.js +0 -300
- package/dist-cjs/adapters/oz/index.d.ts +0 -1
- package/dist-cjs/adapters/oz/index.js +0 -8
- package/dist-cjs/codegen/compile-gate.d.ts +0 -33
- package/dist-cjs/codegen/compile-gate.js +0 -128
- package/dist-cjs/codegen/index.d.ts +0 -2
- package/dist-cjs/codegen/index.js +0 -14
- package/dist-cjs/codegen/template.d.ts +0 -18
- package/dist-cjs/codegen/template.js +0 -151
- 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/dist-cjs/ir/index.d.ts +0 -1
- package/dist-cjs/ir/index.js +0 -3
- package/dist-cjs/ir/types.d.ts +0 -140
- package/dist-cjs/ir/types.js +0 -12
- package/dist-cjs/mandate/index.d.ts +0 -2
- package/dist-cjs/mandate/index.js +0 -6
- package/dist-cjs/mandate/to-ir.d.ts +0 -3
- package/dist-cjs/mandate/to-ir.js +0 -63
- package/dist-cjs/mandate/types.d.ts +0 -20
- package/dist-cjs/mandate/types.js +0 -9
- package/dist-cjs/seams/index.d.ts +0 -1
- package/dist-cjs/seams/index.js +0 -3
- package/dist-cjs/seams/types.d.ts +0 -66
- package/dist-cjs/seams/types.js +0 -12
- package/dist-cjs/synth/deny-cases.js +0 -568
- package/dist-cjs/synth/evaluate.d.ts +0 -39
- package/dist-cjs/synth/evaluate.js +0 -554
- package/dist-cjs/synth/harness.d.ts +0 -28
- package/dist-cjs/synth/harness.js +0 -50
- package/dist-cjs/synth/minimize.d.ts +0 -4
- package/dist-cjs/synth/minimize.js +0 -41
- package/dist-cjs/synth/permit-context.d.ts +0 -15
- package/dist-cjs/synth/permit-context.js +0 -119
- package/dist-cjs/synth/predicate-literals.d.ts +0 -5
- package/dist-cjs/synth/predicate-literals.js +0 -28
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +0 -20
- package/dist-cjs/synth/synthesize-from-mandate.js +0 -62
- package/dist-cjs/verify/envelope.d.ts +0 -15
- package/dist-cjs/verify/envelope.js +0 -23
- package/dist-cjs/verify/index.d.ts +0 -3
- package/dist-cjs/verify/index.js +0 -8
- package/dist-cjs/verify/simulate.d.ts +0 -31
- package/dist-cjs/verify/simulate.js +0 -261
- package/dist-cjs/verify/verify.d.ts +0 -21
- package/dist-cjs/verify/verify.js +0 -192
- 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,7 +1,5 @@
|
|
|
1
|
-
import type { OzAdapterConfig } from '../adapters/oz/adapter.ts';
|
|
2
1
|
import type { ToolError, ToolResponse } from '../errors.ts';
|
|
3
2
|
import { type Network, type PredicateNode, type ProposedPolicy, type RecordedTransaction } from '../types.ts';
|
|
4
|
-
import type { SimulationResult } from '../verify/envelope.ts';
|
|
5
3
|
import { type ComposeUserResponses } from './compose-from-recording.ts';
|
|
6
4
|
/** Per-policy interpreter adapter config the caller opts into. Absent ->
|
|
7
5
|
* recording path runs in week-1 mode (OZ adapter only, warnings for the
|
|
@@ -13,13 +11,6 @@ export interface InterpreterAdapterOptions {
|
|
|
13
11
|
smartAccountAddress: string;
|
|
14
12
|
/** Per-rule install nonce (first install = 1). Defaults to 1. */
|
|
15
13
|
installNonce?: number;
|
|
16
|
-
/** Per-policy oracle overrides. Tighten-only vs the wasm defaults
|
|
17
|
-
* (maxStalenessSeconds <= 600, maxDeviationBps <= 200); widening is
|
|
18
|
-
* rejected at the options boundary. */
|
|
19
|
-
oracleParams?: {
|
|
20
|
-
maxStalenessSeconds?: number;
|
|
21
|
-
maxDeviationBps?: number;
|
|
22
|
-
};
|
|
23
14
|
}
|
|
24
15
|
/** PRIVATE test-only extension of `InterpreterAdapterOptions` for the
|
|
25
16
|
* `__testPredicateNode` seam. Exported under a `__` prefix so production
|
|
@@ -43,7 +34,7 @@ export interface SynthesizeFromRecordingOptions {
|
|
|
43
34
|
* never altered by enabling explain. Absent -> success envelope is byte-identical. */
|
|
44
35
|
explain?: true;
|
|
45
36
|
}
|
|
46
|
-
export declare function synthesizeFromRecording(tx: RecordedTransaction, opts: SynthesizeFromRecordingOptions
|
|
37
|
+
export declare function synthesizeFromRecording(tx: RecordedTransaction, opts: SynthesizeFromRecordingOptions): ToolResponse<ProposedPolicy> & {
|
|
47
38
|
/** Present iff `opts.explain === true` and the synthesis succeeded. The
|
|
48
39
|
* `predicateTree` is the exact in-memory `PredicateNode` (canonical
|
|
49
40
|
* JSON shape) that was encoded into `proposed.policyDocuments[*].encodedPredicate`
|
|
@@ -56,28 +47,9 @@ export declare function synthesizeFromRecording(tx: RecordedTransaction, opts: S
|
|
|
56
47
|
* builds a minimal honest SimulationResult downstream. */
|
|
57
48
|
explain?: {
|
|
58
49
|
predicateTree: PredicateNode | null;
|
|
59
|
-
simulation: SimulationResult;
|
|
60
50
|
};
|
|
61
51
|
};
|
|
62
52
|
/** ToolError-shaped error helper used by the body to surface a structured
|
|
63
53
|
* failure that the envelope converts to `{ok:false, error}`. */
|
|
64
54
|
declare function throwToolError(code: ToolError['code'], message: string): never;
|
|
65
55
|
export { throwToolError };
|
|
66
|
-
/** Diagnostic reported by `visitOracleLeaves` when an oracle comparison
|
|
67
|
-
* cannot be normalised to oracle-on-left. Currently only "oracle-on-right
|
|
68
|
-
* with a non-literal LHS" - the other branches already produce a parseable
|
|
69
|
-
* threshold and are returned to the caller. */
|
|
70
|
-
export interface OracleNormalisationWarning {
|
|
71
|
-
/** The dimension name (currently always "oracle_normalisation_dropped"). */
|
|
72
|
-
dimension: 'oracle_normalisation_dropped';
|
|
73
|
-
/** The operator as written. */
|
|
74
|
-
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte';
|
|
75
|
-
/** The asset the oracle leaf is bound to. */
|
|
76
|
-
asset: string;
|
|
77
|
-
/** The non-literal RHS (oracle-on-left) or LHS (oracle-on-right) leaf
|
|
78
|
-
* whose shape stopped the normalisation. Surfaced for diagnostics;
|
|
79
|
-
* intentionally a partial view - the full leaf is the caller's job. */
|
|
80
|
-
otherKind: string;
|
|
81
|
-
/** Human-readable message (matches the warn-on-drop text). */
|
|
82
|
-
message: string;
|
|
83
|
-
}
|
|
@@ -1,30 +1,25 @@
|
|
|
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
|
|
8
|
-
import {
|
|
9
|
-
import { createOzAdapter } from "../adapters/oz/adapter.js";
|
|
6
|
+
// -> interpreter compile + self-verify.
|
|
7
|
+
// Same (tx, opts) -> byte-identical ProposedPolicy (no randomness, clock, globals).
|
|
8
|
+
import { compileInterpreterPolicy, lowerRuleToPredicate, PLACEHOLDER_INTERPRETER_ADDRESS, } from "../adapters/interpreter/adapter.js";
|
|
10
9
|
import { encodePredicate } from "../predicate/encode.js";
|
|
11
|
-
import {
|
|
10
|
+
import { GRAMMAR_VERSION, OZ_LIMITS, SOROBAN_LIMITS, } from "../types.js";
|
|
12
11
|
import { composeFromRecording, } from "./compose-from-recording.js";
|
|
13
|
-
import { generateCases, ORIGINAL_DIMENSIONS } from "./deny-cases.js";
|
|
14
|
-
import { evaluate } from "./evaluate.js";
|
|
15
|
-
import { runHarness } from "./harness.js";
|
|
16
12
|
import { lower } from "./lower.js";
|
|
17
|
-
import { minimize } from "./minimize.js";
|
|
18
13
|
import { decideScope } from "./scope.js";
|
|
19
|
-
const UNCOVERED_PREFIX = 'Not
|
|
20
|
-
export function synthesizeFromRecording(tx, opts
|
|
14
|
+
const UNCOVERED_PREFIX = 'Not expressible as a predicate constraint: ';
|
|
15
|
+
export function synthesizeFromRecording(tx, opts) {
|
|
21
16
|
// Convert any ToolError-shaped throw (object with a string `.code`) to a
|
|
22
17
|
// structured `{ok:false, error}`; anything else is rethrown so genuine bugs
|
|
23
18
|
// crash instead of being silently swallowed. Wraps the entire body so
|
|
24
|
-
// cap errors (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) and
|
|
19
|
+
// cap errors (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) and encoder depth
|
|
25
20
|
// throws surface as structured ToolErrors rather than RangeErrors.
|
|
26
21
|
try {
|
|
27
|
-
return synthesizeFromRecordingInner(tx, opts
|
|
22
|
+
return synthesizeFromRecordingInner(tx, opts);
|
|
28
23
|
}
|
|
29
24
|
catch (e) {
|
|
30
25
|
if (isToolErrorShape(e)) {
|
|
@@ -60,7 +55,7 @@ function throwToolError(code, message) {
|
|
|
60
55
|
err.retryable = false;
|
|
61
56
|
throw err;
|
|
62
57
|
}
|
|
63
|
-
function synthesizeFromRecordingInner(tx, opts
|
|
58
|
+
function synthesizeFromRecordingInner(tx, opts) {
|
|
64
59
|
// 0. validate inputs (fail closed - never synthesize from garbage).
|
|
65
60
|
const invalid = validateOptions(opts);
|
|
66
61
|
if (invalid)
|
|
@@ -152,7 +147,6 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
152
147
|
const topLevel = tx.invocations[0] ?? null;
|
|
153
148
|
const composeOpts = {
|
|
154
149
|
network: opts.network,
|
|
155
|
-
interpreterEnabled: opts.interpreter !== undefined,
|
|
156
150
|
...(opts.userResponses !== undefined ? { userResponses: opts.userResponses } : {}),
|
|
157
151
|
};
|
|
158
152
|
const composed = composeFromRecording(facts, scope.contract, topLevel, composeOpts);
|
|
@@ -160,37 +154,17 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
160
154
|
// self-verify verdict (built from the SAME runHarness + evaluate that gated
|
|
161
155
|
// the synthesis, not a parallel simulation).
|
|
162
156
|
let explain = null;
|
|
163
|
-
let explainSim = null;
|
|
164
|
-
// 5. OZ compile (always runs).
|
|
165
|
-
const ozAdapter = createOzAdapter(ozConfig);
|
|
166
|
-
const compileRes = ozAdapter.compile(composed.ir);
|
|
167
|
-
if (!compileRes.proposed) {
|
|
168
|
-
return {
|
|
169
|
-
ok: false,
|
|
170
|
-
error: {
|
|
171
|
-
code: 'SYNTHESIS_ERROR',
|
|
172
|
-
message: `recording lowered to no installable OZ policy: ${compileRes.uncovered.join('; ')}`,
|
|
173
|
-
severity: 'error',
|
|
174
|
-
retryable: false,
|
|
175
|
-
details: { uncovered: compileRes.uncovered },
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
157
|
// 6. Interpreter compile (opt-in; fail-closed when the user routed real
|
|
180
158
|
// restrictions to the interpreter).
|
|
181
159
|
const interpreterOpts = opts.interpreter;
|
|
182
160
|
let interpreterPolicyDocument = null;
|
|
183
161
|
let interpreterPolicyRef = null;
|
|
184
|
-
|
|
185
|
-
// warnings folded into `proposed.warnings[]` (which lives after that block)
|
|
186
|
-
// can read it. The block assigns it; the default is empty.
|
|
187
|
-
let permitCtxWarnings = [];
|
|
162
|
+
let interpreterContextRule = null;
|
|
188
163
|
if (interpreterOpts) {
|
|
189
164
|
const interpreterConfig = {
|
|
190
165
|
network: opts.network,
|
|
191
166
|
installNonce: interpreterOpts.installNonce ?? 1,
|
|
192
167
|
smartAccountAddress: interpreterOpts.smartAccountAddress,
|
|
193
|
-
...(interpreterOpts.oracleParams ? { oracleParams: interpreterOpts.oracleParams } : {}),
|
|
194
168
|
};
|
|
195
169
|
let startingPredicate = null;
|
|
196
170
|
// `__testPredicateNode` is a test-only seam. It is NOT in the public
|
|
@@ -207,23 +181,19 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
207
181
|
startingPredicate = testSeam;
|
|
208
182
|
}
|
|
209
183
|
else {
|
|
210
|
-
const interpreterAdapter = createInterpreterAdapter(interpreterConfig);
|
|
211
184
|
let interpreterRes;
|
|
212
185
|
try {
|
|
213
|
-
interpreterRes =
|
|
186
|
+
interpreterRes = compileInterpreterPolicy(composed.interpreterRule, interpreterConfig);
|
|
214
187
|
}
|
|
215
188
|
catch (e) {
|
|
216
189
|
const code = e.code;
|
|
217
190
|
const allowedCodes = [
|
|
218
191
|
'SCOPE_SELF_CALL',
|
|
219
|
-
'ORACLE_LEAF_INVALID_POSITION',
|
|
220
|
-
'ORACLE_PARAMS_OUT_OF_RANGE',
|
|
221
192
|
'MALFORMED_PREDICATE',
|
|
222
193
|
'PREDICATE_TOO_LARGE',
|
|
223
194
|
'PREDICATE_TOO_DEEP',
|
|
224
195
|
'TOO_MANY_LEAVES',
|
|
225
196
|
'IN_OPERAND_LIMIT',
|
|
226
|
-
'PREDICATE_ORACLE_OVER_LIMIT',
|
|
227
197
|
];
|
|
228
198
|
const surfacedCode = code && allowedCodes.includes(code) ? code : 'SYNTHESIS_ERROR';
|
|
229
199
|
return {
|
|
@@ -261,6 +231,7 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
261
231
|
}
|
|
262
232
|
interpreterPolicyDocument = interpreterRes.proposed.policyDocuments[0] ?? null;
|
|
263
233
|
interpreterPolicyRef = interpreterRes.proposed.policyRefs[0] ?? null;
|
|
234
|
+
interpreterContextRule = interpreterRes.proposed.contextRule;
|
|
264
235
|
if (!interpreterPolicyDocument || !interpreterPolicyRef) {
|
|
265
236
|
return {
|
|
266
237
|
ok: false,
|
|
@@ -272,7 +243,7 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
272
243
|
},
|
|
273
244
|
};
|
|
274
245
|
}
|
|
275
|
-
const firstInterpreterRule = composed.
|
|
246
|
+
const firstInterpreterRule = composed.interpreterRule;
|
|
276
247
|
if (!firstInterpreterRule) {
|
|
277
248
|
return {
|
|
278
249
|
ok: false,
|
|
@@ -297,7 +268,9 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
297
268
|
},
|
|
298
269
|
};
|
|
299
270
|
}
|
|
300
|
-
// 6b.
|
|
271
|
+
// 6b. The predicate is used as compiled. There is no minimisation pass:
|
|
272
|
+
// nothing strips a leaf and re-checks that the reduced predicate still
|
|
273
|
+
// denies, so the result is NOT a proven-minimal policy.
|
|
301
274
|
if (!topLevel) {
|
|
302
275
|
return {
|
|
303
276
|
ok: false,
|
|
@@ -320,81 +293,21 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
320
293
|
},
|
|
321
294
|
};
|
|
322
295
|
}
|
|
323
|
-
|
|
324
|
-
// only the oracle-on-right normaliser) and folded into the proposed
|
|
325
|
-
// policy's `warnings[]` so the caller sees them on the success envelope.
|
|
326
|
-
const permitCtxResult = buildPermitContext(tx, scope, topLevel, opts.userResponses, startingPredicate);
|
|
327
|
-
const permitCtx = permitCtxResult.ctx;
|
|
328
|
-
permitCtxWarnings = permitCtxResult.warnings;
|
|
329
|
-
const finalPredicate = startingPredicate.op === 'and'
|
|
330
|
-
? minimize(startingPredicate, permitCtx, ORIGINAL_DIMENSIONS)
|
|
331
|
-
: startingPredicate;
|
|
332
|
-
const harnessCases = generateCases(finalPredicate, permitCtx, ORIGINAL_DIMENSIONS);
|
|
333
|
-
const harnessResult = runHarness(finalPredicate, harnessCases);
|
|
334
|
-
if (!harnessResult.ok) {
|
|
335
|
-
return {
|
|
336
|
-
ok: false,
|
|
337
|
-
error: {
|
|
338
|
-
code: 'DENY_CASE_FAILURE',
|
|
339
|
-
message: `self-verify harness failed for the interpreter predicate (${harnessResult.failures.length} failure(s))`,
|
|
340
|
-
severity: 'error',
|
|
341
|
-
retryable: false,
|
|
342
|
-
details: { failures: harnessResult.failures },
|
|
343
|
-
},
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
const evalResult = evaluate(finalPredicate, permitCtx);
|
|
347
|
-
if (!evalResult.permit) {
|
|
348
|
-
return {
|
|
349
|
-
ok: false,
|
|
350
|
-
error: {
|
|
351
|
-
code: 'DENY_CASE_FAILURE',
|
|
352
|
-
message: `intended recorded call was denied by the interpreter predicate: ${evalResult.reason}`,
|
|
353
|
-
severity: 'error',
|
|
354
|
-
retryable: false,
|
|
355
|
-
details: { reason: evalResult.reason },
|
|
356
|
-
},
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
// --explain capture: quote the SAME verdict that gated the synthesis.
|
|
360
|
-
// Re-evaluate each deny case to surface its concrete reason (the harness
|
|
361
|
-
// only records whether the got-matches-expected boundary held).
|
|
296
|
+
const finalPredicate = startingPredicate;
|
|
362
297
|
if (opts.explain) {
|
|
363
298
|
explain = finalPredicate;
|
|
364
|
-
const evaluatedCases = [
|
|
365
|
-
{
|
|
366
|
-
dimension: 'permit',
|
|
367
|
-
outcome: evalResult.permit ? 'permit' : 'deny',
|
|
368
|
-
reason: 'matches recorded call',
|
|
369
|
-
},
|
|
370
|
-
];
|
|
371
|
-
for (const deny of harnessCases.denies) {
|
|
372
|
-
const r = evaluate(finalPredicate, deny.ctx);
|
|
373
|
-
evaluatedCases.push({
|
|
374
|
-
dimension: deny.dimension,
|
|
375
|
-
outcome: r.permit ? 'permit' : 'deny',
|
|
376
|
-
reason: r.permit ? 'no matching deny' : r.reason,
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
explainSim = {
|
|
380
|
-
permit: { tx: 'permit' },
|
|
381
|
-
evaluatedCases,
|
|
382
|
-
backend: 'ts-model',
|
|
383
|
-
simulatorVersion: 'ts-model-1.0.0',
|
|
384
|
-
};
|
|
385
299
|
}
|
|
386
|
-
// 6c.
|
|
387
|
-
//
|
|
300
|
+
// 6c. Encode the PredicateNode and stamp the canonical bytes back onto
|
|
301
|
+
// the PolicyDocument + PolicyRef. Cap breaches
|
|
388
302
|
// (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) throw ToolError-shaped errors;
|
|
389
303
|
// the outer envelope converts them to structured `{ok:false, error}`.
|
|
390
304
|
const { encodedPredicate, predicateHash } = encodePredicate(finalPredicate);
|
|
391
305
|
if (testSeam !== undefined) {
|
|
392
306
|
interpreterPolicyDocument = {
|
|
393
|
-
grammarVersion:
|
|
307
|
+
grammarVersion: GRAMMAR_VERSION,
|
|
394
308
|
installNonce: interpreterOpts.installNonce ?? 1,
|
|
395
309
|
encodedPredicate,
|
|
396
310
|
predicateHash,
|
|
397
|
-
...(interpreterOpts.oracleParams ? { oracleParams: interpreterOpts.oracleParams } : {}),
|
|
398
311
|
};
|
|
399
312
|
interpreterPolicyRef = {
|
|
400
313
|
kind: 'interpreter',
|
|
@@ -427,20 +340,23 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
427
340
|
}
|
|
428
341
|
}
|
|
429
342
|
}
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
343
|
+
// 7. Assemble the ProposedPolicy. The interpreter is the only backend, so
|
|
344
|
+
// its context rule and policy ref are the whole policy. Without interpreter
|
|
345
|
+
// options nothing installable was produced, which is an error rather than
|
|
346
|
+
// an empty-but-successful policy.
|
|
347
|
+
if (!interpreterContextRule || !interpreterPolicyRef) {
|
|
348
|
+
return {
|
|
349
|
+
ok: false,
|
|
350
|
+
error: {
|
|
351
|
+
code: 'SYNTHESIS_ERROR',
|
|
352
|
+
message: 'no installable policy was synthesised: supply `interpreter` options (a smart account address) so the recording can be lowered to an interpreter predicate',
|
|
353
|
+
severity: 'error',
|
|
354
|
+
retryable: false,
|
|
355
|
+
details: { uncovered: composed.warnings },
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const mergedRefs = [interpreterPolicyRef];
|
|
444
360
|
if (mergedRefs.length > OZ_LIMITS.maxPoliciesPerRule) {
|
|
445
361
|
return {
|
|
446
362
|
ok: false,
|
|
@@ -453,7 +369,7 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
453
369
|
};
|
|
454
370
|
}
|
|
455
371
|
const mergedContextRule = {
|
|
456
|
-
...
|
|
372
|
+
...interpreterContextRule,
|
|
457
373
|
policies: mergedRefs,
|
|
458
374
|
};
|
|
459
375
|
// When nothing installable was synthesised (no interpreter doc AND no OZ
|
|
@@ -470,12 +386,7 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
470
386
|
policyDocuments: interpreterPolicyDocument ? [interpreterPolicyDocument] : [],
|
|
471
387
|
policyRefs: mergedRefs,
|
|
472
388
|
parseConfidence: { ...tx.parseConfidence },
|
|
473
|
-
warnings: [
|
|
474
|
-
...zeroPolicyWarning,
|
|
475
|
-
...ozUncovered.map((u) => `${UNCOVERED_PREFIX}${u}`),
|
|
476
|
-
...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`),
|
|
477
|
-
...permitCtxWarnings,
|
|
478
|
-
],
|
|
389
|
+
warnings: [...zeroPolicyWarning, ...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`)],
|
|
479
390
|
ambiguities: mergeAmbiguities(composed.ambiguities, scope.ambiguities),
|
|
480
391
|
};
|
|
481
392
|
// --explain success envelope. When opts.explain is set and the OZ-only
|
|
@@ -484,41 +395,11 @@ function synthesizeFromRecordingInner(tx, opts, ozConfig) {
|
|
|
484
395
|
// permit is deny with a truthful reason and evaluatedCases is empty.
|
|
485
396
|
const envelope = { ok: true, data: proposed };
|
|
486
397
|
if (opts.explain) {
|
|
487
|
-
|
|
488
|
-
envelope.explain = { predicateTree: explain, simulation: explainSim };
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
envelope.explain = {
|
|
492
|
-
predicateTree: null,
|
|
493
|
-
simulation: {
|
|
494
|
-
permit: {
|
|
495
|
-
tx: 'deny',
|
|
496
|
-
reason: 'No self-verification was performed (interpreter adapter was not engaged)',
|
|
497
|
-
},
|
|
498
|
-
evaluatedCases: [],
|
|
499
|
-
backend: 'ts-model',
|
|
500
|
-
simulatorVersion: 'not-run',
|
|
501
|
-
},
|
|
502
|
-
};
|
|
503
|
-
}
|
|
398
|
+
envelope.explain = { predicateTree: explain };
|
|
504
399
|
}
|
|
505
400
|
return envelope;
|
|
506
401
|
}
|
|
507
402
|
export { throwToolError };
|
|
508
|
-
/** OZ-side `uncovered` warning patterns the interpreter adapter actually lowers
|
|
509
|
-
* when wired in. When the interpreter adapter succeeds, matching entries are
|
|
510
|
-
* dropped from the OZ uncovered list so user-facing warnings reflect what is
|
|
511
|
-
* still UN-enforced rather than what OZ alone could not do. Matches the
|
|
512
|
-
* exact descriptor strings the OZ adapter emits (see `src/adapters/oz/adapter.ts`
|
|
513
|
-
* `describeCondition` / `describeSelector`). */
|
|
514
|
-
const INTERPRETER_COVERED_OZ_PATTERN = /^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/;
|
|
515
|
-
/** Reject non-sane inputs before any policy is synthesized. windowSeconds /
|
|
516
|
-
* validUntilLedger / invocationLimit must be positive integers; limitAmount a
|
|
517
|
-
* positive i128 decimal string; network mainnet|testnet. When the interpreter
|
|
518
|
-
* adapter is opted in, `smartAccountAddress` must be a C... contract address
|
|
519
|
-
* (the on-chain policy-bound account, NOT the G... source account),
|
|
520
|
-
* `installNonce` must fit u32 (default 1), and `oracleParams` must
|
|
521
|
-
* tighten-only vs the wasm defaults. */
|
|
522
403
|
function validateOptions(opts) {
|
|
523
404
|
if (opts.network !== 'mainnet' && opts.network !== 'testnet') {
|
|
524
405
|
return synthesisError(`network must be 'mainnet' or 'testnet', got: ${String(opts.network)}`);
|
|
@@ -531,16 +412,10 @@ function validateOptions(opts) {
|
|
|
531
412
|
}
|
|
532
413
|
const ur = opts.userResponses;
|
|
533
414
|
if (ur) {
|
|
534
|
-
if (ur.windowSeconds !== undefined && !isPositiveInt(ur.windowSeconds)) {
|
|
535
|
-
return synthesisError(`windowSeconds must be a positive integer, got: ${ur.windowSeconds}`);
|
|
536
|
-
}
|
|
537
415
|
if (ur.validUntilLedger !== undefined &&
|
|
538
416
|
(!isPositiveInt(ur.validUntilLedger) || ur.validUntilLedger > SOROBAN_LIMITS.u32Max)) {
|
|
539
417
|
return synthesisError(`validUntilLedger must be a positive u32 ledger sequence (<= ${SOROBAN_LIMITS.u32Max}), got: ${ur.validUntilLedger}`);
|
|
540
418
|
}
|
|
541
|
-
if (ur.invocationLimit !== undefined && !isPositiveInt(ur.invocationLimit)) {
|
|
542
|
-
return synthesisError(`invocationLimit must be a positive integer, got: ${ur.invocationLimit}`);
|
|
543
|
-
}
|
|
544
419
|
if (ur.limitAmount !== undefined && !isPositiveI128(ur.limitAmount)) {
|
|
545
420
|
return synthesisError(`limitAmount must be a positive i128 decimal string within [1, ${I128_MAX}] (2^127-1), got: ${ur.limitAmount}`);
|
|
546
421
|
}
|
|
@@ -564,19 +439,6 @@ function validateOptions(opts) {
|
|
|
564
439
|
if (nonce !== undefined && (!isPositiveInt(nonce) || nonce > SOROBAN_LIMITS.u32Max)) {
|
|
565
440
|
return synthesisError(`interpreter.installNonce must be a positive u32 integer (<= ${SOROBAN_LIMITS.u32Max}), got: ${nonce}`);
|
|
566
441
|
}
|
|
567
|
-
const op = opts.interpreter.oracleParams;
|
|
568
|
-
if (op) {
|
|
569
|
-
const MAX_STALE = 600;
|
|
570
|
-
const MAX_DEV = 200;
|
|
571
|
-
if (op.maxStalenessSeconds !== undefined &&
|
|
572
|
-
(!isPositiveInt(op.maxStalenessSeconds) || op.maxStalenessSeconds > MAX_STALE)) {
|
|
573
|
-
return synthesisError(`interpreter.oracleParams.maxStalenessSeconds must be a positive integer <= ${MAX_STALE} (tighten-only), got: ${op.maxStalenessSeconds}`);
|
|
574
|
-
}
|
|
575
|
-
if (op.maxDeviationBps !== undefined &&
|
|
576
|
-
(!isPositiveInt(op.maxDeviationBps) || op.maxDeviationBps > MAX_DEV)) {
|
|
577
|
-
return synthesisError(`interpreter.oracleParams.maxDeviationBps must be a positive integer <= ${MAX_DEV} (tighten-only), got: ${op.maxDeviationBps}`);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
442
|
}
|
|
581
443
|
return null;
|
|
582
444
|
}
|
|
@@ -630,180 +492,3 @@ function mergeAmbiguities(...lists) {
|
|
|
630
492
|
}
|
|
631
493
|
return out;
|
|
632
494
|
}
|
|
633
|
-
/** Build the permit `EvalContext` the self-verify harness drives. Shape
|
|
634
|
-
* mirrors the intended recorded call so:
|
|
635
|
-
* - `evaluate(predicate, ctx).permit === true` must hold (a failure
|
|
636
|
-
* surfaces as DENY_CASE_FAILURE).
|
|
637
|
-
* - `generateCases(predicate, ctx)` produces a deny battery that reflects
|
|
638
|
-
* the actual recorded move (real amount, args, window start).
|
|
639
|
-
* Amounts are summed per-token over all movements (BigInt, never lossy).
|
|
640
|
-
* `oraclePriceByAsset` contains a price+timestamp satisfying each
|
|
641
|
-
* `oracle_price` leaf so the intended call permits under every bound;
|
|
642
|
-
* the harness mutates those entries (stale / missing / deviation / paused)
|
|
643
|
-
* to exercise the ORACLE_* deny paths. */
|
|
644
|
-
function buildPermitContext(tx, scope, topLevel, userResponses, predicate) {
|
|
645
|
-
const amountByToken = {};
|
|
646
|
-
const totals = new Map();
|
|
647
|
-
for (const m of tx.tokenMovements) {
|
|
648
|
-
const current = totals.get(m.token) ?? 0n;
|
|
649
|
-
totals.set(m.token, current + BigInt(m.amount));
|
|
650
|
-
}
|
|
651
|
-
for (const [token, total] of totals) {
|
|
652
|
-
amountByToken[token] = total.toString();
|
|
653
|
-
}
|
|
654
|
-
const warnings = [];
|
|
655
|
-
const oraclePriceByAsset = oracleSatisfyingPrices(predicate, tx.fetchedAt, warnings);
|
|
656
|
-
const ctx = {
|
|
657
|
-
contract: scope.contract,
|
|
658
|
-
fn: topLevel.fn,
|
|
659
|
-
args: topLevel.args.map(cloneScVal),
|
|
660
|
-
atLedger: tx.ledgerSequence,
|
|
661
|
-
nowSeconds: tx.fetchedAt,
|
|
662
|
-
amountByToken,
|
|
663
|
-
windowSpentByToken: {},
|
|
664
|
-
invocationCountByWindow: {},
|
|
665
|
-
oraclePriceByAsset,
|
|
666
|
-
};
|
|
667
|
-
if (userResponses?.validUntilLedger !== undefined) {
|
|
668
|
-
ctx.validUntilLedger = userResponses.validUntilLedger;
|
|
669
|
-
}
|
|
670
|
-
return { ctx, warnings };
|
|
671
|
-
}
|
|
672
|
-
function cloneScVal(value, depth = 0) {
|
|
673
|
-
// Clone top-level shells so the harness can mutate deny cases without
|
|
674
|
-
// aliasing the recorded call. Recursion bounded by MAX_SCVAL_CLONE_DEPTH so
|
|
675
|
-
// a hand-crafted nested-vec cannot RangeError the JS stack; the over-depth
|
|
676
|
-
// branch throws a ToolError-shaped error the envelope converts to
|
|
677
|
-
// `{ok:false, error}`.
|
|
678
|
-
if (value.type === 'vec') {
|
|
679
|
-
if (depth >= MAX_SCVAL_CLONE_DEPTH) {
|
|
680
|
-
throw cloneDepthError(value);
|
|
681
|
-
}
|
|
682
|
-
return {
|
|
683
|
-
type: 'vec',
|
|
684
|
-
value: value.value.map((v) => cloneScVal(v, depth + 1)),
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
return { ...value };
|
|
688
|
-
}
|
|
689
|
-
function cloneDepthError(value) {
|
|
690
|
-
const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`);
|
|
691
|
-
err.code = 'SYNTHESIS_ERROR';
|
|
692
|
-
err.severity = 'error';
|
|
693
|
-
err.retryable = false;
|
|
694
|
-
err.depthContext = value.type;
|
|
695
|
-
throw err;
|
|
696
|
-
}
|
|
697
|
-
/** Walk every `oracle_price` leaf and return a price map whose entries satisfy
|
|
698
|
-
* the bound. Timestamp pinned to `nowSeconds` (the recorded `fetchedAt`) so
|
|
699
|
-
* the fresh-oracle deny case in `generateCases` is the only path that flips
|
|
700
|
-
* this map. Negatives clamped at 0 - oracle prices are non-negative on Stellar. */
|
|
701
|
-
function oracleSatisfyingPrices(predicate, nowSeconds, warnings) {
|
|
702
|
-
const out = {};
|
|
703
|
-
visitOracleLeaves(predicate, (asset, op, bound) => {
|
|
704
|
-
let price;
|
|
705
|
-
switch (op) {
|
|
706
|
-
case 'lt':
|
|
707
|
-
case 'gt':
|
|
708
|
-
price = op === 'lt' ? bound - 1n : bound + 1n;
|
|
709
|
-
break;
|
|
710
|
-
case 'lte':
|
|
711
|
-
case 'gte':
|
|
712
|
-
case 'eq':
|
|
713
|
-
price = bound;
|
|
714
|
-
break;
|
|
715
|
-
}
|
|
716
|
-
if (price < 0n)
|
|
717
|
-
price = 0n;
|
|
718
|
-
out[asset] = { price: price.toString(), timestampSeconds: nowSeconds };
|
|
719
|
-
}, (warning) => {
|
|
720
|
-
// Cross-layer L3: oracle-on-right that cannot be normalised is
|
|
721
|
-
// surfaced as a warning, not silently dropped. The Rust interpreter
|
|
722
|
-
// would surface this as `UnsupportedNode`; the warning lets the
|
|
723
|
-
// caller decide whether to fix the predicate shape or accept the
|
|
724
|
-
// over-permissive hole.
|
|
725
|
-
warnings.push(warning.message);
|
|
726
|
-
});
|
|
727
|
-
return out;
|
|
728
|
-
}
|
|
729
|
-
function visitOracleLeaves(node, visit, onWarning) {
|
|
730
|
-
switch (node.op) {
|
|
731
|
-
case 'and':
|
|
732
|
-
case 'or':
|
|
733
|
-
for (const child of node.children)
|
|
734
|
-
visitOracleLeaves(child, visit, onWarning);
|
|
735
|
-
return;
|
|
736
|
-
case 'not':
|
|
737
|
-
visitOracleLeaves(node.child, visit, onWarning);
|
|
738
|
-
return;
|
|
739
|
-
case 'eq':
|
|
740
|
-
case 'lt':
|
|
741
|
-
case 'lte':
|
|
742
|
-
case 'gt':
|
|
743
|
-
case 'gte': {
|
|
744
|
-
const leftLeaf = node.left;
|
|
745
|
-
const rightLeaf = node.right;
|
|
746
|
-
const leftIsOracle = leftLeaf.kind === 'oracle_price';
|
|
747
|
-
const rightIsOracle = rightLeaf.kind === 'oracle_price';
|
|
748
|
-
let oracleAsset;
|
|
749
|
-
let literal;
|
|
750
|
-
if (leftIsOracle) {
|
|
751
|
-
oracleAsset = leftLeaf.asset;
|
|
752
|
-
literal = oracleLiteralFromLeaf(rightLeaf);
|
|
753
|
-
}
|
|
754
|
-
else if (rightIsOracle) {
|
|
755
|
-
oracleAsset = rightLeaf.asset;
|
|
756
|
-
literal = oracleLiteralFromLeaf(leftLeaf);
|
|
757
|
-
}
|
|
758
|
-
if (oracleAsset === undefined || literal === undefined) {
|
|
759
|
-
// Two reasons the case cannot be normalised:
|
|
760
|
-
// - neither side is an oracle leaf (visitor has nothing to do)
|
|
761
|
-
// - oracle-on-right with a non-literal LHS (Rust dispatch would
|
|
762
|
-
// hit UnsupportedNode; the TS model cannot build a permit ctx
|
|
763
|
-
// for it)
|
|
764
|
-
// Only the second is a meaningful warning; the first is a no-op
|
|
765
|
-
// (the visitor was called for a non-oracle comparison). Emit the
|
|
766
|
-
// warning when an oracle IS present on one side but the other
|
|
767
|
-
// side is not a parseable threshold literal.
|
|
768
|
-
if (rightIsOracle) {
|
|
769
|
-
onWarning?.({
|
|
770
|
-
dimension: 'oracle_normalisation_dropped',
|
|
771
|
-
op: node.op,
|
|
772
|
-
asset: rightLeaf.kind === 'oracle_price' ? rightLeaf.asset : '',
|
|
773
|
-
otherKind: leftLeaf.kind,
|
|
774
|
-
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`,
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
visit(oracleAsset, node.op, literal);
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
case 'in':
|
|
783
|
-
// `in` is pure membership; oracle leaves inside haystacks are
|
|
784
|
-
// forbidden by the position rule, so there's nothing to set up here.
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
/** Restate an oracle threshold on the normalised 9-dp basis prices use.
|
|
789
|
-
* Thresholds carry their own basis, so reading the digits raw would build
|
|
790
|
-
* a permit context off by 10^(decimals-9) and the intended call would not
|
|
791
|
-
* satisfy its own bound. Mirrors NORMALISED_DECIMALS in oracle.rs. */
|
|
792
|
-
function oracleLiteralFromLeaf(leaf) {
|
|
793
|
-
if (leaf.kind !== 'oracle_threshold')
|
|
794
|
-
return undefined;
|
|
795
|
-
let value;
|
|
796
|
-
try {
|
|
797
|
-
value = BigInt(leaf.value);
|
|
798
|
-
}
|
|
799
|
-
catch {
|
|
800
|
-
return undefined;
|
|
801
|
-
}
|
|
802
|
-
const normalised = 9n;
|
|
803
|
-
const decimals = BigInt(leaf.decimals);
|
|
804
|
-
if (decimals <= normalised)
|
|
805
|
-
return value * 10n ** (normalised - decimals);
|
|
806
|
-
// Floor: a finer-grained threshold has no exact 9-dp representation; the
|
|
807
|
-
// caller offsets by one from here to land on the right side of the bound.
|
|
808
|
-
return value / 10n ** (decimals - normalised);
|
|
809
|
-
}
|