@crediolabs/policy-synth 0.1.17 → 0.2.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 +44 -246
- package/dist/adapters/interpreter/adapter.d.ts +6 -7
- package/dist/adapters/interpreter/adapter.js +28 -48
- package/dist/adapters/oz/adapter.js +12 -13
- package/dist/codegen/compile-gate.js +5 -0
- package/dist/codegen/template.js +17 -0
- package/dist/install/authority-overlap.d.ts +134 -0
- package/dist/install/authority-overlap.js +0 -0
- package/dist/install/build-add-context-rule.d.ts +8 -0
- package/dist/install/build-add-context-rule.js +17 -46
- package/dist/install/build-install-policy.d.ts +12 -5
- package/dist/install/build-install-policy.js +32 -23
- package/dist/install/build-merge-policy.d.ts +70 -0
- package/dist/install/build-merge-policy.js +130 -0
- package/dist/install/get-interpreter-info.js +2 -2
- package/dist/install/index.d.ts +3 -1
- package/dist/install/index.js +8 -1
- package/dist/install/oz-auth.js +5 -3
- package/dist/install/plan-merge-policy.d.ts +49 -0
- package/dist/install/plan-merge-policy.js +86 -0
- package/dist/install/read-account-rules.d.ts +100 -0
- package/dist/install/read-account-rules.js +283 -0
- package/dist/predicate/decode.js +1 -1
- package/dist/predicate/encode.js +171 -10
- package/dist/record/decode.js +5 -6
- package/dist/registry/protocols.d.ts +1 -1
- package/dist/registry/protocols.js +1 -1
- package/dist/review-card/builder.d.ts +6 -0
- package/dist/review-card/builder.js +9 -1
- package/dist/run/index.d.ts +104 -18
- package/dist/run/index.js +359 -75
- package/dist/run/schemas.d.ts +479 -18
- package/dist/run/schemas.js +132 -24
- package/dist/synth/compose-from-recording.d.ts +10 -15
- package/dist/synth/compose-from-recording.js +79 -124
- package/dist/synth/deny-cases.d.ts +5 -0
- package/dist/synth/deny-cases.js +52 -13
- package/dist/synth/evaluate.js +69 -119
- package/dist/synth/harness.d.ts +13 -1
- package/dist/synth/harness.js +22 -1
- package/dist/synth/index.d.ts +1 -1
- package/dist/synth/synthesize-from-recording.d.ts +31 -21
- package/dist/synth/synthesize-from-recording.js +129 -155
- package/dist-cjs/adapters/interpreter/adapter.d.ts +6 -7
- package/dist-cjs/adapters/interpreter/adapter.js +28 -48
- package/dist-cjs/adapters/oz/adapter.js +12 -13
- package/dist-cjs/codegen/compile-gate.js +5 -0
- package/dist-cjs/codegen/template.js +17 -0
- package/dist-cjs/install/authority-overlap.d.ts +134 -0
- package/dist-cjs/install/authority-overlap.js +0 -0
- package/dist-cjs/install/build-add-context-rule.d.ts +8 -0
- package/dist-cjs/install/build-add-context-rule.js +17 -45
- package/dist-cjs/install/build-install-policy.d.ts +12 -5
- package/dist-cjs/install/build-install-policy.js +32 -23
- package/dist-cjs/install/build-merge-policy.d.ts +70 -0
- package/dist-cjs/install/build-merge-policy.js +134 -0
- package/dist-cjs/install/get-interpreter-info.js +2 -2
- package/dist-cjs/install/index.d.ts +3 -1
- package/dist-cjs/install/index.js +24 -3
- package/dist-cjs/install/oz-auth.js +5 -3
- package/dist-cjs/install/plan-merge-policy.d.ts +49 -0
- package/dist-cjs/install/plan-merge-policy.js +90 -0
- package/dist-cjs/install/read-account-rules.d.ts +100 -0
- package/dist-cjs/install/read-account-rules.js +296 -0
- package/dist-cjs/predicate/decode.js +1 -1
- package/dist-cjs/predicate/encode.js +171 -10
- package/dist-cjs/record/decode.js +5 -6
- package/dist-cjs/registry/protocols.d.ts +1 -1
- package/dist-cjs/registry/protocols.js +1 -1
- package/dist-cjs/review-card/builder.d.ts +6 -0
- package/dist-cjs/review-card/builder.js +9 -1
- package/dist-cjs/run/index.d.ts +104 -18
- package/dist-cjs/run/index.js +360 -74
- package/dist-cjs/run/schemas.d.ts +479 -18
- package/dist-cjs/run/schemas.js +133 -25
- package/dist-cjs/synth/compose-from-recording.d.ts +10 -15
- package/dist-cjs/synth/compose-from-recording.js +79 -124
- package/dist-cjs/synth/deny-cases.d.ts +5 -0
- package/dist-cjs/synth/deny-cases.js +52 -13
- package/dist-cjs/synth/evaluate.js +69 -119
- package/dist-cjs/synth/harness.d.ts +13 -1
- package/dist-cjs/synth/harness.js +22 -1
- package/dist-cjs/synth/index.d.ts +1 -1
- package/dist-cjs/synth/synthesize-from-recording.d.ts +31 -21
- package/dist-cjs/synth/synthesize-from-recording.js +129 -153
- package/package.json +1 -1
- package/src/adapters/interpreter/adapter.ts +28 -48
- package/src/adapters/oz/adapter.ts +12 -13
- package/src/codegen/compile-gate.ts +5 -0
- package/src/codegen/template.ts +17 -0
- package/src/install/authority-overlap.ts +0 -0
- package/src/install/build-add-context-rule.ts +29 -56
- package/src/install/build-install-policy.ts +41 -23
- package/src/install/build-merge-policy.ts +219 -0
- package/src/install/get-interpreter-info.ts +2 -2
- package/src/install/index.ts +36 -2
- package/src/install/oz-auth.ts +5 -3
- package/src/install/plan-merge-policy.ts +133 -0
- package/src/install/read-account-rules.ts +376 -0
- package/src/predicate/decode.ts +1 -1
- package/src/predicate/encode.ts +176 -10
- package/src/record/decode.ts +5 -6
- package/src/registry/protocols.ts +1 -1
- package/src/review-card/builder.ts +17 -1
- package/src/run/index.ts +500 -102
- package/src/run/schemas.ts +145 -24
- package/src/synth/compose-from-recording.ts +87 -132
- package/src/synth/deny-cases.ts +59 -13
- package/src/synth/evaluate.ts +70 -118
- package/src/synth/harness.ts +29 -1
- package/src/synth/index.ts +1 -0
- package/src/synth/synthesize-from-recording.ts +209 -192
- package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +0 -196
|
@@ -1,35 +1,10 @@
|
|
|
1
1
|
// src/synth/synthesize-from-recording.ts - recording-path orchestrator.
|
|
2
2
|
//
|
|
3
|
-
// `synthesizeFromRecording`
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// the caller may relax via `confidenceOverride.threshold`).
|
|
9
|
-
// 2. `lower(tx)` -> IntentFacts.
|
|
10
|
-
// 3. `decideScope(facts)` -> scope | SCOPE_UNRESOLVED ToolError.
|
|
11
|
-
// 4. `composeFromRecording(facts, scope, opts)` -> { ir (OZ-shape),
|
|
12
|
-
// interpreterIr (predicate-shape), ambiguities, warnings }.
|
|
13
|
-
// 5. `ozAdapter.compile(ir)` -> CompileResult.
|
|
14
|
-
// 6. If `interpreterIr.rules[0].constraints.length > 0` AND the caller
|
|
15
|
-
// opted in to the interpreter adapter:
|
|
16
|
-
// a. Run `interpreterAdapter.compile(interpreterIr)`.
|
|
17
|
-
// b. Throw -> SYNTHESIS_ERROR carrying the gate code
|
|
18
|
-
// (SCOPE_SELF_CALL, ORACLE_LEAF_INVALID_POSITION,
|
|
19
|
-
// ORACLE_PARAMS_OUT_OF_RANGE). Never install an OZ-only partial
|
|
20
|
-
// policy - a warning can be ignored and yields an over-broad rule.
|
|
21
|
-
// c. `covered === false` -> SYNTHESIS_ERROR carrying the uncovered
|
|
22
|
-
// descriptor (the user asked for the interpreter to enforce these
|
|
23
|
-
// constraints; refusing them silently is the audit failure mode).
|
|
24
|
-
// d. `covered === true` -> merge its PolicyDocument + PolicyRef into
|
|
25
|
-
// the OZ-shaped ProposedPolicy; the merged `contextRule.policies`
|
|
26
|
-
// is `[interpreterRef?, ...oz_builtinRefs]` and must satisfy
|
|
27
|
-
// `OZ_LIMITS.maxPoliciesPerRule` (= 5).
|
|
28
|
-
// 7. Assemble ProposedPolicy carrying parseConfidence (from tx), warnings
|
|
29
|
-
// (the OZ `uncovered` markers, prefixed), and ambiguities.
|
|
30
|
-
//
|
|
31
|
-
// Determinism: same (tx, opts, ozConfig) -> byte-identical ProposedPolicy.
|
|
32
|
-
// No randomness, no clock, no globals.
|
|
3
|
+
// `synthesizeFromRecording` INFERS a bounded policy from a `RecordedTransaction`
|
|
4
|
+
// via the same `PolicyIR` + adapter pair used by the deterministic Mandate path.
|
|
5
|
+
// Flow: validate -> parseConfidence gate -> lower -> decideScope -> composeFromRecording
|
|
6
|
+
// -> OZ compile -> (opt-in) interpreter compile + self-verify + minimise -> merge.
|
|
7
|
+
// Same (tx, opts, ozConfig) -> byte-identical ProposedPolicy (no randomness, clock, globals).
|
|
33
8
|
|
|
34
9
|
import type { InterpreterAdapterConfig } from '../adapters/interpreter/adapter.ts'
|
|
35
10
|
import {
|
|
@@ -81,34 +56,27 @@ export interface InterpreterAdapterOptions {
|
|
|
81
56
|
* (maxStalenessSeconds <= 600, maxDeviationBps <= 200); widening is
|
|
82
57
|
* rejected at the options boundary. */
|
|
83
58
|
oracleParams?: { maxStalenessSeconds?: number; maxDeviationBps?: number }
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** PRIVATE test-only extension of `InterpreterAdapterOptions` for the
|
|
62
|
+
* `__testPredicateNode` seam. Exported under a `__` prefix so production
|
|
63
|
+
* callers can grep for it and see it is a test hook. The seam is read in
|
|
64
|
+
* `synthesizeFromRecordingInner` only when `process.env.NODE_ENV === 'test'`
|
|
65
|
+
* - a hard RUNTIME guard throws otherwise so a misconfigured production
|
|
66
|
+
* caller cannot bypass the recording -> interpreter adapter compile path. */
|
|
67
|
+
export type __TestInterpreterAdapterOptions = InterpreterAdapterOptions & {
|
|
68
|
+
/** @internal Test-only seam. Throws at runtime when NODE_ENV !== 'test'. */
|
|
93
69
|
__testPredicateNode?: PredicateNode
|
|
94
70
|
}
|
|
95
71
|
|
|
96
|
-
/** Top-level orchestrator inputs.
|
|
97
|
-
* answers to the ambiguity prompts (windowSeconds, validUntilLedger,
|
|
98
|
-
* limitAmount, invocationLimit, oraclePriceBound, swapRecipientAllowlist).
|
|
99
|
-
* `interpreter` opts the recording path into the interpreter adapter. */
|
|
72
|
+
/** Top-level orchestrator inputs. */
|
|
100
73
|
export interface SynthesizeFromRecordingOptions {
|
|
101
74
|
network: Network
|
|
102
75
|
userResponses?: ComposeUserResponses
|
|
103
76
|
confidenceOverride?: { threshold: number }
|
|
104
77
|
interpreter?: InterpreterAdapterOptions
|
|
105
|
-
/** --explain opt-in.
|
|
106
|
-
*
|
|
107
|
-
* to the success envelope so the CLI can render a human-readable
|
|
108
|
-
* review card. Absent or false -> the success envelope is unchanged
|
|
109
|
-
* (byte-identical to today). The flag is ADDITIVE: the existing
|
|
110
|
-
* ProposedPolicy fields (encodedPredicate, predicateHash, etc.) are
|
|
111
|
-
* never altered by enabling explain. */
|
|
78
|
+
/** --explain opt-in. The flag is ADDITIVE: existing ProposedPolicy fields are
|
|
79
|
+
* never altered by enabling explain. Absent -> success envelope is byte-identical. */
|
|
112
80
|
explain?: true
|
|
113
81
|
}
|
|
114
82
|
|
|
@@ -132,13 +100,11 @@ export function synthesizeFromRecording(
|
|
|
132
100
|
simulation: SimulationResult
|
|
133
101
|
}
|
|
134
102
|
} {
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
// TOO_MANY_LEAVES, etc.) or a depth-cap throw from `cloneScVal` surfaces as
|
|
141
|
-
// a structured ToolError rather than a thrown RangeError.
|
|
103
|
+
// Convert any ToolError-shaped throw (object with a string `.code`) to a
|
|
104
|
+
// structured `{ok:false, error}`; anything else is rethrown so genuine bugs
|
|
105
|
+
// crash instead of being silently swallowed. Wraps the entire body so
|
|
106
|
+
// cap errors (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) and cloneScVal depth
|
|
107
|
+
// throws surface as structured ToolErrors rather than RangeErrors.
|
|
142
108
|
try {
|
|
143
109
|
return synthesizeFromRecordingInner(tx, opts, ozConfig)
|
|
144
110
|
} catch (e) {
|
|
@@ -157,10 +123,9 @@ export function synthesizeFromRecording(
|
|
|
157
123
|
}
|
|
158
124
|
}
|
|
159
125
|
|
|
160
|
-
/** True when `e` is
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* the body's internal helpers agree on. */
|
|
126
|
+
/** True when `e` is a ToolError-shaped throw (object with a string `code`).
|
|
127
|
+
* Detected by string `code` field — the only contract the body's internal
|
|
128
|
+
* helpers agree on. */
|
|
164
129
|
function isToolErrorShape(e: unknown): e is { code: string; message: string } {
|
|
165
130
|
if (e === null || typeof e !== 'object') return false
|
|
166
131
|
const code = (e as { code?: unknown }).code
|
|
@@ -178,7 +143,6 @@ function throwToolError(code: ToolError['code'], message: string): never {
|
|
|
178
143
|
throw err
|
|
179
144
|
}
|
|
180
145
|
|
|
181
|
-
/** Synthesize a ProposedPolicy from a recorded transaction. */
|
|
182
146
|
function synthesizeFromRecordingInner(
|
|
183
147
|
tx: RecordedTransaction,
|
|
184
148
|
opts: SynthesizeFromRecordingOptions,
|
|
@@ -193,11 +157,9 @@ function synthesizeFromRecordingInner(
|
|
|
193
157
|
const invalid = validateOptions(opts)
|
|
194
158
|
if (invalid) return { ok: false, error: invalid }
|
|
195
159
|
|
|
196
|
-
// 0a. per-movement amount validation
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
// one - reject it here so the downstream `BigInt(m.amount)` in
|
|
200
|
-
// `buildPermitContext` cannot throw a SyntaxError past the envelope.
|
|
160
|
+
// 0a. per-movement amount validation. Reject malformed recordings here so
|
|
161
|
+
// the downstream `BigInt(m.amount)` in `buildPermitContext` cannot throw
|
|
162
|
+
// a SyntaxError past the envelope.
|
|
201
163
|
for (const [i, m] of tx.tokenMovements.entries()) {
|
|
202
164
|
if (!/^[0-9]+$/.test(m.amount)) {
|
|
203
165
|
return {
|
|
@@ -227,15 +189,9 @@ function synthesizeFromRecordingInner(
|
|
|
227
189
|
}
|
|
228
190
|
}
|
|
229
191
|
|
|
230
|
-
// 1a. Zero-invocation refusal
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
// policy must scope to an authorized contract call. Refuse before any
|
|
234
|
-
// lower/scope work so the failure is specific and actionable. The
|
|
235
|
-
// recorder's silence is also made visible via `parseConfidence.noInvocations`
|
|
236
|
-
// so consumers can pattern-match without inferring from `invocations: []`
|
|
237
|
-
// next to `overall: 1.0`. The message does NOT ask for an ABI - the
|
|
238
|
-
// failure mode is the recording shape, not decoding coverage.
|
|
192
|
+
// 1a. Zero-invocation refusal. A recording with zero invocations clears
|
|
193
|
+
// parseConfidence legitimately (denom === 0 short-circuits overall to 1.0),
|
|
194
|
+
// but a policy must scope to an authorized contract call. Refuse early.
|
|
239
195
|
const hasNoInvocations = tx.parseConfidence.noInvocations === true || tx.invocations.length === 0
|
|
240
196
|
if (hasNoInvocations) {
|
|
241
197
|
return {
|
|
@@ -268,10 +224,8 @@ function synthesizeFromRecordingInner(
|
|
|
268
224
|
}
|
|
269
225
|
}
|
|
270
226
|
|
|
271
|
-
// 2. lower.
|
|
272
227
|
const facts = lower(tx)
|
|
273
228
|
|
|
274
|
-
// 3. decideScope.
|
|
275
229
|
const scopeRes = decideScope(facts, {
|
|
276
230
|
network: opts.network,
|
|
277
231
|
...(opts.userResponses?.validUntilLedger !== undefined
|
|
@@ -302,11 +256,8 @@ function synthesizeFromRecordingInner(
|
|
|
302
256
|
const composed = composeFromRecording(facts, scope.contract, topLevel, composeOpts)
|
|
303
257
|
|
|
304
258
|
// --explain hook: capture the in-memory predicate tree + the real
|
|
305
|
-
// self-verify verdict
|
|
306
|
-
//
|
|
307
|
-
// already gates the synthesis (it is not a parallel simulation); the
|
|
308
|
-
// intermediate inputs (harnessCases, evalResult) are otherwise discarded
|
|
309
|
-
// after the gate, so the explain hook reuses them - no extra work.
|
|
259
|
+
// self-verify verdict (built from the SAME runHarness + evaluate that gated
|
|
260
|
+
// the synthesis, not a parallel simulation).
|
|
310
261
|
let explain: PredicateNode | null = null
|
|
311
262
|
let explainSim: SimulationResult | null = null
|
|
312
263
|
|
|
@@ -333,6 +284,10 @@ function synthesizeFromRecordingInner(
|
|
|
333
284
|
|
|
334
285
|
let interpreterPolicyDocument: ProposedPolicy['policyDocuments'][number] | null = null
|
|
335
286
|
let interpreterPolicyRef: ProposedPolicy['policyRefs'][number] | null = null
|
|
287
|
+
// Cross-layer L3: declared OUTSIDE the `if (interpreterOpts)` block so the
|
|
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[] = []
|
|
336
291
|
|
|
337
292
|
if (interpreterOpts) {
|
|
338
293
|
const interpreterConfig: InterpreterAdapterConfig = {
|
|
@@ -343,8 +298,19 @@ function synthesizeFromRecordingInner(
|
|
|
343
298
|
}
|
|
344
299
|
|
|
345
300
|
let startingPredicate: PredicateNode | null = null
|
|
346
|
-
|
|
301
|
+
// `__testPredicateNode` is a test-only seam. It is NOT in the public
|
|
302
|
+
// `InterpreterAdapterOptions` type, so a production caller cannot set it
|
|
303
|
+
// without bypassing the type system. We read it via a private cast and
|
|
304
|
+
// enforce a runtime NODE_ENV check so a misconfigured production caller
|
|
305
|
+
// that smuggles it in (any-cast, JSON-driven opt, etc.) is caught here
|
|
306
|
+
// rather than silently overriding the compiled predicate.
|
|
307
|
+
const testSeam = (interpreterOpts as __TestInterpreterAdapterOptions).__testPredicateNode
|
|
347
308
|
if (testSeam !== undefined) {
|
|
309
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
310
|
+
throw new Error(
|
|
311
|
+
'synthesizeFromRecording: __testPredicateNode is a test-only seam and is refused outside NODE_ENV=test'
|
|
312
|
+
)
|
|
313
|
+
}
|
|
348
314
|
startingPredicate = testSeam
|
|
349
315
|
} else {
|
|
350
316
|
const interpreterAdapter = createInterpreterAdapter(interpreterConfig)
|
|
@@ -466,7 +432,18 @@ function synthesizeFromRecordingInner(
|
|
|
466
432
|
},
|
|
467
433
|
}
|
|
468
434
|
}
|
|
469
|
-
|
|
435
|
+
// Cross-layer L3: warnings collected from `buildPermitContext` (currently
|
|
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
|
|
470
447
|
|
|
471
448
|
const finalPredicate: PredicateNode =
|
|
472
449
|
startingPredicate.op === 'and'
|
|
@@ -501,12 +478,9 @@ function synthesizeFromRecordingInner(
|
|
|
501
478
|
}
|
|
502
479
|
}
|
|
503
480
|
|
|
504
|
-
// --explain capture: the
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
// quotes the SAME verdict that gated the synthesis. We re-evaluate
|
|
508
|
-
// each deny case to surface its concrete reason; the harness only
|
|
509
|
-
// records whether the got-matches-expected boundary held.
|
|
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).
|
|
510
484
|
if (opts.explain) {
|
|
511
485
|
explain = finalPredicate
|
|
512
486
|
const evaluatedCases: SimulationResult['evaluatedCases'] = [
|
|
@@ -533,10 +507,9 @@ function synthesizeFromRecordingInner(
|
|
|
533
507
|
}
|
|
534
508
|
|
|
535
509
|
// 6c. Re-encode the (possibly minimised) PredicateNode and stamp the
|
|
536
|
-
// canonical bytes back onto the PolicyDocument + PolicyRef.
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
// envelope converts them to a structured `{ok:false, error}`.
|
|
510
|
+
// canonical bytes back onto the PolicyDocument + PolicyRef. Cap breaches
|
|
511
|
+
// (PREDICATE_TOO_DEEP, TOO_MANY_LEAVES) throw ToolError-shaped errors;
|
|
512
|
+
// the outer envelope converts them to structured `{ok:false, error}`.
|
|
540
513
|
const { encodedPredicate, predicateHash } = encodePredicate(finalPredicate)
|
|
541
514
|
|
|
542
515
|
if (testSeam !== undefined) {
|
|
@@ -612,9 +585,7 @@ function synthesizeFromRecordingInner(
|
|
|
612
585
|
// When nothing installable was synthesised (no interpreter doc AND no OZ
|
|
613
586
|
// policy refs), an empty `policies` array reads as "no restrictions" rather
|
|
614
587
|
// than "I synthesised nothing". Surface that explicitly so the empty result
|
|
615
|
-
// is never mistaken for a permissive policy
|
|
616
|
-
// but it constrains nothing. (Kept as `{ok:true}` so the documented
|
|
617
|
-
// Path-A/Path-B demo behaviour is preserved - see F3.)
|
|
588
|
+
// is never mistaken for a permissive policy.
|
|
618
589
|
const zeroPolicyWarning =
|
|
619
590
|
mergedRefs.length === 0 && !interpreterPolicyDocument
|
|
620
591
|
? [
|
|
@@ -631,16 +602,14 @@ function synthesizeFromRecordingInner(
|
|
|
631
602
|
...zeroPolicyWarning,
|
|
632
603
|
...ozUncovered.map((u) => `${UNCOVERED_PREFIX}${u}`),
|
|
633
604
|
...composed.warnings.map((w) => `${UNCOVERED_PREFIX}${w}`),
|
|
605
|
+
...permitCtxWarnings,
|
|
634
606
|
],
|
|
635
607
|
ambiguities: mergeAmbiguities(composed.ambiguities, scope.ambiguities),
|
|
636
608
|
}
|
|
637
|
-
// --explain success envelope.
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
// is
|
|
641
|
-
// SimulationResult: the verdict is NOT a passing simulation - the
|
|
642
|
-
// interpreter was never engaged, so permit is deny with a truthful
|
|
643
|
-
// reason and evaluatedCases is empty.
|
|
609
|
+
// --explain success envelope. When opts.explain is set and the OZ-only
|
|
610
|
+
// path ran, construct the minimal honest SimulationResult: the verdict
|
|
611
|
+
// is NOT a passing simulation - the interpreter was never engaged, so
|
|
612
|
+
// permit is deny with a truthful reason and evaluatedCases is empty.
|
|
644
613
|
const envelope: ToolResponse<ProposedPolicy> & {
|
|
645
614
|
explain?: {
|
|
646
615
|
predicateTree: PredicateNode | null
|
|
@@ -668,27 +637,24 @@ function synthesizeFromRecordingInner(
|
|
|
668
637
|
return envelope
|
|
669
638
|
}
|
|
670
639
|
|
|
671
|
-
// Re-export the throwToolError helper for callers that need to surface a
|
|
672
|
-
// ToolError-shaped throw inside the body (e.g. tests).
|
|
673
640
|
export { throwToolError }
|
|
674
641
|
|
|
675
|
-
/** OZ-side `uncovered` warning patterns
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
* `describeSelector`). */
|
|
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`). */
|
|
682
648
|
const INTERPRETER_COVERED_OZ_PATTERN =
|
|
683
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/
|
|
684
650
|
|
|
685
651
|
/** Reject non-sane inputs before any policy is synthesized. windowSeconds /
|
|
686
652
|
* validUntilLedger / invocationLimit must be positive integers; limitAmount a
|
|
687
|
-
* positive i128 decimal string; network mainnet|testnet. When the
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
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. */
|
|
692
658
|
function validateOptions(opts: SynthesizeFromRecordingOptions): ToolError | null {
|
|
693
659
|
if (opts.network !== 'mainnet' && opts.network !== 'testnet') {
|
|
694
660
|
return synthesisError(`network must be 'mainnet' or 'testnet', got: ${String(opts.network)}`)
|
|
@@ -732,13 +698,9 @@ function validateOptions(opts: SynthesizeFromRecordingOptions): ToolError | null
|
|
|
732
698
|
`interpreter.smartAccountAddress must be a non-empty string, got: ${String(sa)}`
|
|
733
699
|
)
|
|
734
700
|
}
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
// C... contract" message). A real install routes a C... contract
|
|
739
|
-
// address derivable from the on-chain account; any
|
|
740
|
-
// 'VERIFY-*' / 'PLACEHOLDER-*' / 'TODO-*' prefix is an LLM seam /
|
|
741
|
-
// fixture marker that must never reach the install payload.
|
|
701
|
+
// Block placeholder/stub prefixes BEFORE the C.../56-char shape check so
|
|
702
|
+
// a fixture/LLM-seam marker is reported with the specific placeholder
|
|
703
|
+
// error. 'VERIFY-*' / 'PLACEHOLDER-*' / 'TODO-*' must never reach install.
|
|
742
704
|
if (PLACEHOLDER_SMART_ACCOUNT_PREFIX.test(sa)) {
|
|
743
705
|
return synthesisError(
|
|
744
706
|
`interpreter.smartAccountAddress must not be a placeholder/stub address (matches /${PLACEHOLDER_SMART_ACCOUNT_PREFIX.source}/), got: ${sa}`
|
|
@@ -750,9 +712,7 @@ function validateOptions(opts: SynthesizeFromRecordingOptions): ToolError | null
|
|
|
750
712
|
)
|
|
751
713
|
}
|
|
752
714
|
const nonce = opts.interpreter.installNonce
|
|
753
|
-
//
|
|
754
|
-
// u32; values above SOROBAN_LIMITS.u32Max cannot be installed). Mirrors
|
|
755
|
-
// the validUntilLedger SOROBAN_LIMITS.u32Max check above.
|
|
715
|
+
// installNonce must fit u32 (the on-chain per-rule nonce is a u32).
|
|
756
716
|
if (nonce !== undefined && (!isPositiveInt(nonce) || nonce > SOROBAN_LIMITS.u32Max)) {
|
|
757
717
|
return synthesisError(
|
|
758
718
|
`interpreter.installNonce must be a positive u32 integer (<= ${SOROBAN_LIMITS.u32Max}), got: ${nonce}`
|
|
@@ -783,7 +743,7 @@ function validateOptions(opts: SynthesizeFromRecordingOptions): ToolError | null
|
|
|
783
743
|
return null
|
|
784
744
|
}
|
|
785
745
|
|
|
786
|
-
/** Placeholder/stub smart-account prefixes
|
|
746
|
+
/** Placeholder/stub smart-account prefixes. Mirrors the
|
|
787
747
|
* `PLACEHOLDER_INTERPRETER_ADDRESS` marker the interpreter adapter uses for
|
|
788
748
|
* the interpreter-contract strkey; a real install must point at a C...
|
|
789
749
|
* contract address derivable from the on-chain account, never a
|
|
@@ -792,18 +752,17 @@ const PLACEHOLDER_SMART_ACCOUNT_PREFIX = /^(VERIFY-|PLACEHOLDER-|TODO-)/i
|
|
|
792
752
|
|
|
793
753
|
/** Maximum value a signed i128 can hold (2^127-1). A limitAmount above this
|
|
794
754
|
* cannot be represented on-chain, so reject it at the synthesis boundary
|
|
795
|
-
* (fail-closed) instead of passing it through as an over-broad spending_limit.
|
|
796
|
-
* Mirrors the SOROBAN_LIMITS.u32Max bound the ledger-sequence fields enforce. */
|
|
755
|
+
* (fail-closed) instead of passing it through as an over-broad spending_limit. */
|
|
797
756
|
const I128_MAX = 2n ** 127n - 1n
|
|
798
757
|
|
|
799
758
|
function isPositiveInt(n: number): boolean {
|
|
800
759
|
return Number.isInteger(n) && n > 0
|
|
801
760
|
}
|
|
802
761
|
|
|
803
|
-
/** True when `s` is a canonical positive decimal integer inside
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
762
|
+
/** True when `s` is a canonical positive decimal integer inside [1, 2^127-1].
|
|
763
|
+
* Values above the i128 ceiling are rejected fail-closed: they cannot be
|
|
764
|
+
* installed on-chain, and accepting them would emit a spending_limit with an
|
|
765
|
+
* effectively unbounded cap. */
|
|
807
766
|
function isPositiveI128(s: string): boolean {
|
|
808
767
|
if (!/^[0-9]+$/.test(s)) return false
|
|
809
768
|
try {
|
|
@@ -840,27 +799,24 @@ function mergeAmbiguities(
|
|
|
840
799
|
return out
|
|
841
800
|
}
|
|
842
801
|
|
|
843
|
-
/** Build the permit `EvalContext` the self-verify harness drives.
|
|
844
|
-
* mirrors the intended recorded call
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
* permits the recorded call it was derived from); a failure surfaces
|
|
848
|
-
* as DENY_CASE_FAILURE.
|
|
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).
|
|
849
806
|
* - `generateCases(predicate, ctx)` produces a deny battery that reflects
|
|
850
|
-
* the actual recorded move (real amount,
|
|
851
|
-
* Amounts are summed per-token over all movements
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
* exercise the ORACLE_* deny paths. */
|
|
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. */
|
|
857
813
|
function buildPermitContext(
|
|
858
814
|
tx: RecordedTransaction,
|
|
859
815
|
scope: Extract<ScopeDecision, { kind: 'call_contract' }>,
|
|
860
816
|
topLevel: ContractInvocation,
|
|
861
817
|
userResponses: ComposeUserResponses | undefined,
|
|
862
818
|
predicate: PredicateNode
|
|
863
|
-
): EvalContext {
|
|
819
|
+
): { ctx: EvalContext; warnings: string[] } {
|
|
864
820
|
const amountByToken: Record<string, string> = {}
|
|
865
821
|
const totals = new Map<string, bigint>()
|
|
866
822
|
for (const m of tx.tokenMovements) {
|
|
@@ -871,9 +827,11 @@ function buildPermitContext(
|
|
|
871
827
|
amountByToken[token] = total.toString()
|
|
872
828
|
}
|
|
873
829
|
|
|
830
|
+
const warnings: string[] = []
|
|
874
831
|
const oraclePriceByAsset: EvalContext['oraclePriceByAsset'] = oracleSatisfyingPrices(
|
|
875
832
|
predicate,
|
|
876
|
-
tx.fetchedAt
|
|
833
|
+
tx.fetchedAt,
|
|
834
|
+
warnings
|
|
877
835
|
)
|
|
878
836
|
|
|
879
837
|
const ctx: EvalContext = {
|
|
@@ -890,20 +848,18 @@ function buildPermitContext(
|
|
|
890
848
|
if (userResponses?.validUntilLedger !== undefined) {
|
|
891
849
|
ctx.validUntilLedger = userResponses.validUntilLedger
|
|
892
850
|
}
|
|
893
|
-
return ctx
|
|
851
|
+
return { ctx, warnings }
|
|
894
852
|
}
|
|
895
853
|
|
|
896
854
|
function cloneScVal(
|
|
897
855
|
value: { type: string; value: unknown },
|
|
898
856
|
depth = 0
|
|
899
857
|
): ContractInvocation['args'][number] {
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
// ToolError-shaped error that the `synthesizeFromRecording` envelope
|
|
906
|
-
// (item 3) converts to `{ok:false, error}`.
|
|
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}`.
|
|
907
863
|
if (value.type === 'vec') {
|
|
908
864
|
if (depth >= MAX_SCVAL_CLONE_DEPTH) {
|
|
909
865
|
throw cloneDepthError(value)
|
|
@@ -918,9 +874,6 @@ function cloneScVal(
|
|
|
918
874
|
return { ...value } as ContractInvocation['args'][number]
|
|
919
875
|
}
|
|
920
876
|
|
|
921
|
-
/** Throw a ToolError-shaped error when the clone exceeds MAX_SCVAL_CLONE_DEPTH.
|
|
922
|
-
* The shape (object with string `code`) is what the `synthesizeFromRecording`
|
|
923
|
-
* envelope detects and converts to a structured `{ok:false, error}`. */
|
|
924
877
|
function cloneDepthError(value: { type: string; value: unknown }): never {
|
|
925
878
|
const err = new Error(
|
|
926
879
|
`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`
|
|
@@ -928,52 +881,96 @@ function cloneDepthError(value: { type: string; value: unknown }): never {
|
|
|
928
881
|
err.code = 'SYNTHESIS_ERROR'
|
|
929
882
|
err.severity = 'error'
|
|
930
883
|
err.retryable = false
|
|
931
|
-
// Tag the offending value so failures are diagnosable.
|
|
932
884
|
err.depthContext = value.type
|
|
933
885
|
throw err
|
|
934
886
|
}
|
|
935
887
|
|
|
936
|
-
/** Walk every `oracle_price` leaf
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
* this map. Negatives are clamped at 0 - oracle prices are non-negative on
|
|
941
|
-
* Stellar. */
|
|
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. */
|
|
942
892
|
function oracleSatisfyingPrices(
|
|
943
893
|
predicate: PredicateNode,
|
|
944
|
-
nowSeconds: number
|
|
894
|
+
nowSeconds: number,
|
|
895
|
+
warnings: string[]
|
|
945
896
|
): EvalContext['oraclePriceByAsset'] {
|
|
946
897
|
const out: EvalContext['oraclePriceByAsset'] = {}
|
|
947
|
-
visitOracleLeaves(
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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)
|
|
959
923
|
}
|
|
960
|
-
|
|
961
|
-
out[asset] = { price: price.toString(), timestampSeconds: nowSeconds }
|
|
962
|
-
})
|
|
924
|
+
)
|
|
963
925
|
return out
|
|
964
926
|
}
|
|
965
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
|
+
|
|
966
962
|
function visitOracleLeaves(
|
|
967
963
|
node: PredicateNode,
|
|
968
|
-
visit: (asset: string, op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte', bound: bigint) => void
|
|
964
|
+
visit: (asset: string, op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte', bound: bigint) => void,
|
|
965
|
+
onWarning?: (warning: OracleNormalisationWarning) => void
|
|
969
966
|
): void {
|
|
970
967
|
switch (node.op) {
|
|
971
968
|
case 'and':
|
|
972
969
|
case 'or':
|
|
973
|
-
for (const child of node.children) visitOracleLeaves(child, visit)
|
|
970
|
+
for (const child of node.children) visitOracleLeaves(child, visit, onWarning)
|
|
974
971
|
return
|
|
975
972
|
case 'not':
|
|
976
|
-
visitOracleLeaves(node.child, visit)
|
|
973
|
+
visitOracleLeaves(node.child, visit, onWarning)
|
|
977
974
|
return
|
|
978
975
|
case 'eq':
|
|
979
976
|
case 'lt':
|
|
@@ -993,7 +990,27 @@ function visitOracleLeaves(
|
|
|
993
990
|
oracleAsset = rightLeaf.asset
|
|
994
991
|
literal = oracleLiteralFromLeaf(leftLeaf)
|
|
995
992
|
}
|
|
996
|
-
if (oracleAsset === undefined || literal === undefined)
|
|
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
|
+
}
|
|
997
1014
|
visit(oracleAsset, node.op, literal)
|
|
998
1015
|
return
|
|
999
1016
|
}
|
|
@@ -1004,9 +1021,9 @@ function visitOracleLeaves(
|
|
|
1004
1021
|
}
|
|
1005
1022
|
}
|
|
1006
1023
|
|
|
1007
|
-
/**
|
|
1008
|
-
* Thresholds carry their own basis, so reading the digits raw would build
|
|
1009
|
-
* permit context off by 10^(decimals-9) and the intended call would not
|
|
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
|
|
1010
1027
|
* satisfy its own bound. Mirrors NORMALISED_DECIMALS in oracle.rs. */
|
|
1011
1028
|
function oracleLiteralFromLeaf(leaf: PredicateLeaf): bigint | undefined {
|
|
1012
1029
|
if (leaf.kind !== 'oracle_threshold') return undefined
|