@crediolabs/policy-synth 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +193 -5
- package/dist/adapters/interpreter/adapter.d.ts +38 -0
- package/dist/adapters/interpreter/adapter.js +522 -0
- package/dist/adapters/interpreter/index.d.ts +1 -0
- package/dist/adapters/interpreter/index.js +2 -0
- package/dist/adapters/oz/adapter.js +2 -0
- package/dist/codegen/compile-gate.d.ts +33 -0
- package/dist/codegen/compile-gate.js +119 -0
- package/dist/codegen/index.d.ts +2 -0
- package/dist/codegen/index.js +8 -0
- package/dist/codegen/template.d.ts +18 -0
- package/dist/codegen/template.js +131 -0
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/ir/types.d.ts +13 -2
- package/dist/predicate/encode.d.ts +10 -0
- package/dist/predicate/encode.js +249 -0
- package/dist/predicate/index.d.ts +1 -0
- package/dist/predicate/index.js +2 -0
- package/dist/record/decode.d.ts +6 -3
- package/dist/record/decode.js +13 -3
- package/dist/review-card/builder.d.ts +14 -0
- package/dist/review-card/builder.js +261 -0
- package/dist/review-card/conflict.d.ts +40 -0
- package/dist/review-card/conflict.js +111 -0
- package/dist/review-card/cross-check.d.ts +11 -0
- package/dist/review-card/cross-check.js +148 -0
- package/dist/review-card/index.d.ts +3 -0
- package/dist/review-card/index.js +4 -0
- package/dist/run/index.d.ts +31 -0
- package/dist/run/index.js +164 -0
- package/dist/run/schemas.d.ts +2003 -0
- package/dist/run/schemas.js +215 -0
- package/dist/synth/compose-from-recording.d.ts +35 -7
- package/dist/synth/compose-from-recording.js +225 -34
- package/dist/synth/deny-cases.d.ts +12 -0
- package/dist/synth/deny-cases.js +377 -0
- package/dist/synth/evaluate.d.ts +39 -0
- package/dist/synth/evaluate.js +425 -0
- package/dist/synth/harness.d.ts +16 -0
- package/dist/synth/harness.js +26 -0
- package/dist/synth/index.d.ts +4 -0
- package/dist/synth/index.js +4 -0
- package/dist/synth/minimize.d.ts +4 -0
- package/dist/synth/minimize.js +38 -0
- package/dist/synth/predicate-literals.d.ts +5 -0
- package/dist/synth/predicate-literals.js +25 -0
- package/dist/synth/synthesize-from-recording.d.ts +37 -5
- package/dist/synth/synthesize-from-recording.js +550 -18
- package/dist/types.d.ts +40 -1
- package/dist/types.js +17 -0
- package/dist/verify/envelope.d.ts +15 -0
- package/dist/verify/envelope.js +22 -0
- package/dist/verify/index.d.ts +3 -0
- package/dist/verify/index.js +3 -0
- package/dist/verify/simulate.d.ts +31 -0
- package/dist/verify/simulate.js +258 -0
- package/dist/verify/verify.d.ts +21 -0
- package/dist/verify/verify.js +189 -0
- package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
- package/dist-cjs/adapters/interpreter/adapter.js +527 -0
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
- package/dist-cjs/adapters/interpreter/index.js +8 -0
- package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
- package/dist-cjs/adapters/oz/adapter.js +289 -0
- package/dist-cjs/adapters/oz/index.d.ts +1 -0
- package/dist-cjs/adapters/oz/index.js +8 -0
- package/dist-cjs/codegen/compile-gate.d.ts +33 -0
- package/dist-cjs/codegen/compile-gate.js +123 -0
- package/dist-cjs/codegen/index.d.ts +2 -0
- package/dist-cjs/codegen/index.js +14 -0
- package/dist-cjs/codegen/template.d.ts +18 -0
- package/dist-cjs/codegen/template.js +134 -0
- package/dist-cjs/errors.d.ts +37 -0
- package/dist-cjs/errors.js +3 -0
- package/dist-cjs/index.d.ts +11 -0
- package/dist-cjs/index.js +27 -0
- package/dist-cjs/ir/index.d.ts +1 -0
- package/dist-cjs/ir/index.js +3 -0
- package/dist-cjs/ir/types.d.ts +108 -0
- package/dist-cjs/ir/types.js +12 -0
- package/dist-cjs/mandate/index.d.ts +2 -0
- package/dist-cjs/mandate/index.js +6 -0
- package/dist-cjs/mandate/to-ir.d.ts +3 -0
- package/dist-cjs/mandate/to-ir.js +63 -0
- package/dist-cjs/mandate/types.d.ts +20 -0
- package/dist-cjs/mandate/types.js +9 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/predicate/encode.d.ts +10 -0
- package/dist-cjs/predicate/encode.js +252 -0
- package/dist-cjs/predicate/index.d.ts +1 -0
- package/dist-cjs/predicate/index.js +6 -0
- package/dist-cjs/record/decode.d.ts +79 -0
- package/dist-cjs/record/decode.js +398 -0
- package/dist-cjs/record/freshness.d.ts +17 -0
- package/dist-cjs/record/freshness.js +55 -0
- package/dist-cjs/record/index.d.ts +21 -0
- package/dist-cjs/record/index.js +166 -0
- package/dist-cjs/record/movements.d.ts +20 -0
- package/dist-cjs/record/movements.js +192 -0
- package/dist-cjs/record/rpc.d.ts +22 -0
- package/dist-cjs/record/rpc.js +74 -0
- package/dist-cjs/record/validate.d.ts +22 -0
- package/dist-cjs/record/validate.js +63 -0
- package/dist-cjs/registry/identify.d.ts +11 -0
- package/dist-cjs/registry/identify.js +87 -0
- package/dist-cjs/registry/index.d.ts +3 -0
- package/dist-cjs/registry/index.js +15 -0
- package/dist-cjs/registry/known-addresses.d.ts +16 -0
- package/dist-cjs/registry/known-addresses.js +53 -0
- package/dist-cjs/registry/protocols.d.ts +38 -0
- package/dist-cjs/registry/protocols.js +153 -0
- package/dist-cjs/review-card/builder.d.ts +14 -0
- package/dist-cjs/review-card/builder.js +264 -0
- package/dist-cjs/review-card/conflict.d.ts +40 -0
- package/dist-cjs/review-card/conflict.js +114 -0
- package/dist-cjs/review-card/cross-check.d.ts +11 -0
- package/dist-cjs/review-card/cross-check.js +151 -0
- package/dist-cjs/review-card/index.d.ts +3 -0
- package/dist-cjs/review-card/index.js +10 -0
- package/dist-cjs/run/index.d.ts +31 -0
- package/dist-cjs/run/index.js +178 -0
- package/dist-cjs/run/schemas.d.ts +2003 -0
- package/dist-cjs/run/schemas.js +218 -0
- package/dist-cjs/seams/index.d.ts +1 -0
- package/dist-cjs/seams/index.js +3 -0
- package/dist-cjs/seams/types.d.ts +66 -0
- package/dist-cjs/seams/types.js +12 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +64 -0
- package/dist-cjs/synth/compose-from-recording.js +356 -0
- package/dist-cjs/synth/deny-cases.d.ts +12 -0
- package/dist-cjs/synth/deny-cases.js +380 -0
- package/dist-cjs/synth/evaluate.d.ts +39 -0
- package/dist-cjs/synth/evaluate.js +428 -0
- package/dist-cjs/synth/harness.d.ts +16 -0
- package/dist-cjs/synth/harness.js +29 -0
- package/dist-cjs/synth/index.d.ts +9 -0
- package/dist-cjs/synth/index.js +23 -0
- package/dist-cjs/synth/lower.d.ts +23 -0
- package/dist-cjs/synth/lower.js +119 -0
- package/dist-cjs/synth/minimize.d.ts +4 -0
- package/dist-cjs/synth/minimize.js +41 -0
- package/dist-cjs/synth/predicate-literals.d.ts +5 -0
- package/dist-cjs/synth/predicate-literals.js +28 -0
- package/dist-cjs/synth/scope.d.ts +26 -0
- package/dist-cjs/synth/scope.js +81 -0
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
- package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
- package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
- package/dist-cjs/synth/synthesize-from-recording.js +711 -0
- package/dist-cjs/types.d.ts +288 -0
- package/dist-cjs/types.js +55 -0
- package/dist-cjs/verify/envelope.d.ts +15 -0
- package/dist-cjs/verify/envelope.js +23 -0
- package/dist-cjs/verify/index.d.ts +3 -0
- package/dist-cjs/verify/index.js +8 -0
- package/dist-cjs/verify/simulate.d.ts +31 -0
- package/dist-cjs/verify/simulate.js +261 -0
- package/dist-cjs/verify/verify.d.ts +21 -0
- package/dist-cjs/verify/verify.js +192 -0
- package/package.json +43 -3
- package/src/adapters/interpreter/adapter.ts +642 -0
- package/src/adapters/interpreter/index.ts +8 -0
- package/src/adapters/oz/adapter.ts +2 -0
- package/src/codegen/compile-gate.ts +162 -0
- package/src/codegen/index.ts +17 -0
- package/src/codegen/template.ts +148 -0
- package/src/errors.ts +2 -0
- package/src/index.ts +2 -0
- package/src/ir/types.ts +9 -2
- package/src/predicate/encode.ts +307 -0
- package/src/predicate/index.ts +3 -0
- package/src/record/decode.ts +21 -10
- package/src/review-card/builder.ts +303 -0
- package/src/review-card/conflict.ts +143 -0
- package/src/review-card/cross-check.ts +158 -0
- package/src/review-card/index.ts +12 -0
- package/src/run/index.ts +213 -0
- package/src/run/schemas.ts +242 -0
- package/src/synth/compose-from-recording.ts +277 -43
- package/src/synth/deny-cases.ts +466 -0
- package/src/synth/evaluate.ts +493 -0
- package/src/synth/harness.ts +40 -0
- package/src/synth/index.ts +12 -0
- package/src/synth/minimize.ts +44 -0
- package/src/synth/predicate-literals.ts +27 -0
- package/src/synth/synthesize-from-recording.ts +655 -19
- package/src/types.ts +38 -2
- package/src/verify/envelope.ts +28 -0
- package/src/verify/index.ts +5 -0
- package/src/verify/simulate.ts +311 -0
- package/src/verify/verify.ts +243 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// packages/policy-synth/src/run/schemas.ts
|
|
2
|
+
//
|
|
3
|
+
// Zod schemas mirroring the policy-synth core domain types. These are the
|
|
4
|
+
// public input / output shapes exposed over MCP and the CLI. They are kept
|
|
5
|
+
// hand-written (rather than derived) because the MCP SDK needs a runtime
|
|
6
|
+
// Zod object at the transport boundary; a drift test asserts they stay in
|
|
7
|
+
// step with the TS source of truth.
|
|
8
|
+
//
|
|
9
|
+
// i128 amounts and other large integers are carried as base-10 decimal strings
|
|
10
|
+
// end-to-end (no JS number coercion). Networks are pinned to the same closed
|
|
11
|
+
// set the core defines. The discriminated union on `source` exposes BOTH
|
|
12
|
+
// synthesize_policy front-ends through a single tool input.
|
|
13
|
+
//
|
|
14
|
+
// This module is the SINGLE source of truth for these shapes. The MCP package
|
|
15
|
+
// imports them here so its tool-shape bindings stay in step; the CLI imports
|
|
16
|
+
// them here so it can build the same args envelope the MCP transport builds.
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
/** Soroban `valid_until` is a u32 ledger sequence; a value above this cannot be
|
|
19
|
+
* installed on-chain, so reject it at the boundary (fail-closed). */
|
|
20
|
+
const U32_MAX = 4294967295;
|
|
21
|
+
/** Upper bound on top-level invocations in a recorded transaction. A real
|
|
22
|
+
* Stellar tx caps operations well below this; the bound stops a hand-crafted
|
|
23
|
+
* payload from turning one request into an unbounded synthesis (DoS). */
|
|
24
|
+
const MAX_INVOCATIONS = 512;
|
|
25
|
+
export const NetworkSchema = z.enum(['mainnet', 'testnet']);
|
|
26
|
+
/** ScVal subset - normalised subset the synth consumes. Mirrors
|
|
27
|
+
* `ScVal` in packages/policy-synth/src/types.ts. */
|
|
28
|
+
export const ScValSchema = z.lazy(() => z.union([
|
|
29
|
+
z.object({ type: z.literal('address'), value: z.string() }),
|
|
30
|
+
// i128 is SIGNED: real events carry negatives (e.g. a fee-adjustment/refund),
|
|
31
|
+
// so the recorder's own output must round-trip through this schema. u64/u32
|
|
32
|
+
// are unsigned and stay non-negative.
|
|
33
|
+
z.object({ type: z.literal('i128'), value: z.string().regex(/^-?[0-9]+$/) }),
|
|
34
|
+
z.object({ type: z.literal('u64'), value: z.string().regex(/^[0-9]+$/) }),
|
|
35
|
+
z.object({ type: z.literal('u32'), value: z.string().regex(/^[0-9]+$/) }),
|
|
36
|
+
z.object({ type: z.literal('symbol'), value: z.string() }),
|
|
37
|
+
z.object({ type: z.literal('vec'), value: z.array(ScValSchema) }),
|
|
38
|
+
z.object({ type: z.literal('bytes'), value: z.string() }),
|
|
39
|
+
z.object({ type: z.literal('other'), value: z.string() }),
|
|
40
|
+
]));
|
|
41
|
+
/** ContractInvocation mirrors the core. Annotated with an explicit
|
|
42
|
+
* `z.ZodType<unknown>` (like ScValSchema above) so the self-referential
|
|
43
|
+
* `subInvocations` field does not trip TS's circular type inference. */
|
|
44
|
+
export const ContractInvocationSchema = z.object({
|
|
45
|
+
contract: z.string(),
|
|
46
|
+
fn: z.string(),
|
|
47
|
+
args: z.array(ScValSchema),
|
|
48
|
+
subInvocations: z.array(z.lazy(() => ContractInvocationSchema)),
|
|
49
|
+
});
|
|
50
|
+
export const TokenMovementSchema = z.object({
|
|
51
|
+
token: z.string(),
|
|
52
|
+
from: z.string(),
|
|
53
|
+
to: z.string(),
|
|
54
|
+
// The recorder reads the amount straight from the signed i128 event value
|
|
55
|
+
// (record/movements.ts readAmount), so a non-standard token that emits a
|
|
56
|
+
// negative transfer/mint/burn amount round-trips as a negative string. Mirror
|
|
57
|
+
// that here; the synth gate, not the wire schema, decides what to do with it.
|
|
58
|
+
amount: z.string().regex(/^-?[0-9]+$/),
|
|
59
|
+
});
|
|
60
|
+
export const OnChainEventSchema = z.object({
|
|
61
|
+
contract: z.string(),
|
|
62
|
+
topics: z.array(z.string()),
|
|
63
|
+
data: ScValSchema,
|
|
64
|
+
});
|
|
65
|
+
export const ParseConfidenceSchema = z.object({
|
|
66
|
+
overall: z.number().min(0).max(1),
|
|
67
|
+
knownContracts: z.array(z.string()),
|
|
68
|
+
unknownContracts: z.array(z.object({
|
|
69
|
+
contract: z.string(),
|
|
70
|
+
reason: z.enum(['no-abi', 'version-mismatch', 'opaque-result']),
|
|
71
|
+
})),
|
|
72
|
+
opaqueScVals: z.array(z.object({ path: z.string(), type: z.string() })),
|
|
73
|
+
thresholdUsed: z.number().min(0).max(1),
|
|
74
|
+
});
|
|
75
|
+
/** RecordedTransaction mirrors the core RecordedTransaction. The output shape
|
|
76
|
+
* is referenced by name in the tool result structured content; we deliberately
|
|
77
|
+
* type it loosely (`z.unknown()`) on the success path so the core remains the
|
|
78
|
+
* single source of truth for the wire payload. */
|
|
79
|
+
export const RecordedTransactionSchema = z
|
|
80
|
+
.object({
|
|
81
|
+
network: NetworkSchema,
|
|
82
|
+
signers: z.array(z.string()),
|
|
83
|
+
invocations: z.array(ContractInvocationSchema).max(MAX_INVOCATIONS),
|
|
84
|
+
tokenMovements: z.array(TokenMovementSchema),
|
|
85
|
+
events: z.array(OnChainEventSchema),
|
|
86
|
+
authEntries: z.array(z.unknown()),
|
|
87
|
+
ledgerSequence: z.number().int().nonnegative(),
|
|
88
|
+
fetchedAt: z.number().int().nonnegative(),
|
|
89
|
+
parseConfidence: ParseConfidenceSchema,
|
|
90
|
+
sourceAccount: z.string(),
|
|
91
|
+
})
|
|
92
|
+
.passthrough();
|
|
93
|
+
/** MandateSpec mirrors the core MandateSpec. The deterministic Mandate
|
|
94
|
+
* front-end needs no parseConfidence; the tool adapter injects the full
|
|
95
|
+
* confidence after synthesis so the orchestrator can compare. */
|
|
96
|
+
export const MandateSpecSchema = z
|
|
97
|
+
.object({
|
|
98
|
+
chain: z.literal('stellar'),
|
|
99
|
+
contract: z.string(),
|
|
100
|
+
method: z.string().optional(),
|
|
101
|
+
spendingLimit: z
|
|
102
|
+
.object({
|
|
103
|
+
token: z.string(),
|
|
104
|
+
limit: z.string().regex(/^[0-9]+$/),
|
|
105
|
+
windowSeconds: z.number().int().positive(),
|
|
106
|
+
})
|
|
107
|
+
.optional(),
|
|
108
|
+
// A threshold of 0 means "0 approvals", which is not a real M-of-N gate.
|
|
109
|
+
approvalThreshold: z.number().int().positive().optional(),
|
|
110
|
+
recipients: z.array(z.string()).optional(),
|
|
111
|
+
expiry: z
|
|
112
|
+
.object({
|
|
113
|
+
validUntilLedger: z.number().int().positive().max(U32_MAX).optional(),
|
|
114
|
+
validUntilUnixSeconds: z.number().int().positive().optional(),
|
|
115
|
+
})
|
|
116
|
+
.optional(),
|
|
117
|
+
})
|
|
118
|
+
.passthrough();
|
|
119
|
+
/** ComposeUserResponses mirrors the core. */
|
|
120
|
+
export const ComposeUserResponsesSchema = z
|
|
121
|
+
.object({
|
|
122
|
+
windowSeconds: z.number().int().positive().optional(),
|
|
123
|
+
validUntilLedger: z.number().int().positive().max(U32_MAX).optional(),
|
|
124
|
+
limitAmount: z
|
|
125
|
+
.string()
|
|
126
|
+
.regex(/^[0-9]+$/)
|
|
127
|
+
.optional(),
|
|
128
|
+
invocationLimit: z.number().int().positive().optional(),
|
|
129
|
+
})
|
|
130
|
+
.passthrough();
|
|
131
|
+
/** OzAdapterConfig - the per-network OZ built-in instance addresses. */
|
|
132
|
+
export const OzAdapterConfigSchema = z.object({
|
|
133
|
+
network: NetworkSchema,
|
|
134
|
+
instances: z.object({
|
|
135
|
+
spending_limit: z.string(),
|
|
136
|
+
simple_threshold: z.string(),
|
|
137
|
+
weighted_threshold: z.string(),
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
// ===== record_transaction =====
|
|
141
|
+
export const RecordTransactionInputSchema = z
|
|
142
|
+
.object({
|
|
143
|
+
hash: z.string().min(1).optional(),
|
|
144
|
+
xdr: z.string().min(1).optional(),
|
|
145
|
+
network: NetworkSchema,
|
|
146
|
+
confidenceOverride: z.number().min(0).max(1).optional(),
|
|
147
|
+
})
|
|
148
|
+
.refine((v) => !(v.hash && v.xdr), {
|
|
149
|
+
message: 'provide exactly one of `hash` or `xdr`, not both',
|
|
150
|
+
})
|
|
151
|
+
.refine((v) => Boolean(v.hash) || Boolean(v.xdr), {
|
|
152
|
+
message: 'one of `hash` or `xdr` is required',
|
|
153
|
+
});
|
|
154
|
+
// ===== synthesize_policy =====
|
|
155
|
+
//
|
|
156
|
+
// Discriminated union on `source` exposes BOTH front-ends through ONE tool.
|
|
157
|
+
// - `source: 'mandate'` -> calls synthesizeFromMandate
|
|
158
|
+
// - `source: 'recording'` -> calls synthesizeFromRecording
|
|
159
|
+
export const SynthesizePolicyMandateInputSchema = z.object({
|
|
160
|
+
source: z.literal('mandate'),
|
|
161
|
+
mandate: MandateSpecSchema,
|
|
162
|
+
ozConfig: OzAdapterConfigSchema.optional(),
|
|
163
|
+
});
|
|
164
|
+
/** Interpreter opt-in for the recording path. Present -> constraints OZ cannot
|
|
165
|
+
* express (per-method scoping, invocation-count windows, oracle bounds, exact
|
|
166
|
+
* hop paths) lower to a real interpreter predicate document instead of being
|
|
167
|
+
* surfaced as warnings. The core deep-validates `smartAccountAddress` (a C...
|
|
168
|
+
* contract, not the recording's G... source) and the tighten-only oracle
|
|
169
|
+
* bounds; the schema stays light so the core owns the friendly ToolErrors. */
|
|
170
|
+
export const InterpreterOptionsSchema = z.object({
|
|
171
|
+
smartAccountAddress: z.string(),
|
|
172
|
+
installNonce: z.number().int().positive().optional(),
|
|
173
|
+
oracleParams: z
|
|
174
|
+
.object({
|
|
175
|
+
maxStalenessSeconds: z.number().int().positive().optional(),
|
|
176
|
+
maxDeviationBps: z.number().int().positive().optional(),
|
|
177
|
+
})
|
|
178
|
+
.optional(),
|
|
179
|
+
});
|
|
180
|
+
export const SynthesizePolicyRecordingInputSchema = z.object({
|
|
181
|
+
source: z.literal('recording'),
|
|
182
|
+
recordedTx: RecordedTransactionSchema,
|
|
183
|
+
network: NetworkSchema,
|
|
184
|
+
userResponses: ComposeUserResponsesSchema.optional(),
|
|
185
|
+
confidenceOverride: z.object({ threshold: z.number().min(0).max(1) }).optional(),
|
|
186
|
+
interpreter: InterpreterOptionsSchema.optional(),
|
|
187
|
+
ozConfig: OzAdapterConfigSchema.optional(),
|
|
188
|
+
});
|
|
189
|
+
export const SynthesizePolicyInputSchema = z.discriminatedUnion('source', [
|
|
190
|
+
SynthesizePolicyMandateInputSchema,
|
|
191
|
+
SynthesizePolicyRecordingInputSchema,
|
|
192
|
+
]);
|
|
193
|
+
// ===== Error envelope (canonical) =====
|
|
194
|
+
//
|
|
195
|
+
// Mirrors ToolError from packages/policy-synth/src/errors.ts. We use a
|
|
196
|
+
// `z.string()` for `code` (not an enum) because the core's ErrorCode union
|
|
197
|
+
// evolves over time; the transport contract only promises a string code the
|
|
198
|
+
// caller can dispatch on. A drift test asserts the canonical codes still
|
|
199
|
+
// pass through unchanged.
|
|
200
|
+
export const ToolErrorSchema = z
|
|
201
|
+
.object({
|
|
202
|
+
code: z.string(),
|
|
203
|
+
message: z.string(),
|
|
204
|
+
severity: z.enum(['info', 'warning', 'error', 'fatal']),
|
|
205
|
+
retryable: z.boolean(),
|
|
206
|
+
remediation: z
|
|
207
|
+
.object({
|
|
208
|
+
toolCall: z.object({ name: z.string(), args: z.record(z.unknown()) }).optional(),
|
|
209
|
+
userQuestion: z.object({ code: z.string(), question: z.string() }).optional(),
|
|
210
|
+
docsUrl: z.string().optional(),
|
|
211
|
+
})
|
|
212
|
+
.optional(),
|
|
213
|
+
details: z.unknown().optional(),
|
|
214
|
+
})
|
|
215
|
+
.passthrough();
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import type { PolicyIR } from '../ir/types.ts';
|
|
1
|
+
import type { IRCompOp, PolicyIR } from '../ir/types.ts';
|
|
2
2
|
import type { AmbiguityPrompt, ContractInvocation, Network } from '../types.ts';
|
|
3
3
|
import type { IntentFacts } from './lower.ts';
|
|
4
|
+
/** Per-asset oracle-price bound supplied by the caller (e.g. swap allowed only
|
|
5
|
+
* if oracle_price(XLM) < 5.00 USDC). One entry per asset; the recorder never
|
|
6
|
+
* fabricates a price bound from a slippage value (different units). */
|
|
7
|
+
export interface OraclePriceBound {
|
|
8
|
+
asset: string;
|
|
9
|
+
operator: IRCompOp;
|
|
10
|
+
value: string;
|
|
11
|
+
}
|
|
4
12
|
/** Caller-supplied answers to the ambiguity prompts. Every numeric bound the
|
|
5
13
|
* synth might apply must come from here - the recording supplies observed
|
|
6
14
|
* amounts (offered only as suggestions), never authorised ceilings. */
|
|
@@ -15,22 +23,42 @@ export interface ComposeUserResponses {
|
|
|
15
23
|
/** Max invocations per window for an incoming-only flow. Required to emit an
|
|
16
24
|
* invocation_count bound; absent -> FREQUENCY_BOUND_MISSING. */
|
|
17
25
|
invocationLimit?: number;
|
|
26
|
+
/** Per-asset oracle-price bound(s). Each entry lowers to a single
|
|
27
|
+
* `oracle_price(asset) OP value` compare in the interpreter IR. Multiple
|
|
28
|
+
* entries on the same asset emit multiple leaves. */
|
|
29
|
+
oraclePriceBound?: OraclePriceBound[];
|
|
30
|
+
/** Recipient allowlist for a swap (call_arg[3] on SoroSwap's
|
|
31
|
+
* swap_exact_tokens_for_tokens). Absent -> RECIPIENT_ALLOWLIST_EMPTY
|
|
32
|
+
* ambiguity (the caller is prompted; proceeding without an allowlist leaves
|
|
33
|
+
* the recipient unconstrained). */
|
|
34
|
+
swapRecipientAllowlist?: string[];
|
|
18
35
|
}
|
|
19
36
|
/** Composition options. */
|
|
20
37
|
export interface ComposeOptions {
|
|
21
38
|
network: Network;
|
|
22
39
|
userResponses?: ComposeUserResponses;
|
|
40
|
+
/** When true, constraints the OZ adapter cannot lower are routed to
|
|
41
|
+
* `interpreterIr` (the predicate-shape IR) so the orchestrator can compile
|
|
42
|
+
* them via the interpreter adapter. When false (the default for callers
|
|
43
|
+
* who have not opted in), every constraint goes to `ir` and the OZ
|
|
44
|
+
* adapter's `uncovered` machinery generates the descriptive warnings -
|
|
45
|
+
* today's behaviour. The orchestrator passes this flag through based on
|
|
46
|
+
* whether `opts.interpreter` was supplied. */
|
|
47
|
+
interpreterEnabled?: boolean;
|
|
23
48
|
}
|
|
24
|
-
/** Result of composition: the PolicyIR,
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
49
|
+
/** Result of composition: the OZ-shape PolicyIR, the predicate-shape PolicyIR
|
|
50
|
+
* (contains the constraints the OZ adapter cannot lower; empty when
|
|
51
|
+
* `interpreterEnabled` is false), any ambiguities surfaced during inference,
|
|
52
|
+
* and descriptive warnings for needs that are NOT expressed as an IR node
|
|
53
|
+
* (so no fabricated constraint is emitted). The orchestrator carries
|
|
54
|
+
* ambiguities into `ProposedPolicy.ambiguities` and merges warnings into
|
|
55
|
+
* `ProposedPolicy.warnings`. */
|
|
29
56
|
export interface ComposeResult {
|
|
30
57
|
ir: PolicyIR;
|
|
58
|
+
interpreterIr: PolicyIR;
|
|
31
59
|
ambiguities: AmbiguityPrompt[];
|
|
32
60
|
warnings: string[];
|
|
33
61
|
}
|
|
34
|
-
/** Compose a PolicyIR from the lowered facts + the resolved scope.
|
|
62
|
+
/** Compose a PolicyIR pair from the lowered facts + the resolved scope.
|
|
35
63
|
* Pure (no randomness, no clock); same inputs -> byte-identical result. */
|
|
36
64
|
export declare function composeFromRecording(facts: IntentFacts, scopeContract: string, topLevel: ContractInvocation | null, opts: ComposeOptions): ComposeResult;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// src/synth/compose-from-recording.ts - facts + scope -> PolicyIR.
|
|
1
|
+
// src/synth/compose-from-recording.ts - facts + scope -> PolicyIR (OZ-shape + interpreter-shape).
|
|
2
2
|
//
|
|
3
|
-
// Composes the canonical IR
|
|
3
|
+
// Composes the canonical IR rules the two backends compile FROM. Fail-closed
|
|
4
4
|
// composition rules:
|
|
5
5
|
//
|
|
6
6
|
// - identify the protocol of the top-level call (registry.identifyProtocol).
|
|
@@ -32,14 +32,30 @@
|
|
|
32
32
|
// price fabricated from a slippage bound, no synthetic exact-path compare.
|
|
33
33
|
// Those needs are surfaced as descriptive warnings instead.
|
|
34
34
|
//
|
|
35
|
+
// Split rule (P3 wiring): `ComposeResult` carries BOTH `ir` (OZ-shape) and
|
|
36
|
+
// `interpreterIr` (predicate-shape). Each constraint is routed to EXACTLY ONE
|
|
37
|
+
// adapter:
|
|
38
|
+
// - `compare window_spent(token, w) <= limit` where `token === scope.contract`
|
|
39
|
+
// and the protocol is known -> `ir` (OZ lowers to spending_limit).
|
|
40
|
+
// - everything else the compose step emits (recipient allowlists, per-method
|
|
41
|
+
// scoping via scope.method, invocation_count bounds, eq_seq swap paths,
|
|
42
|
+
// oracle_price bounds, AND window_spent where token != scope.contract, i.e.
|
|
43
|
+
// a SoroSwap input-token cap) -> `interpreterIr`.
|
|
44
|
+
//
|
|
45
|
+
// This prevents the interpreter adapter from emitting a duplicate
|
|
46
|
+
// `window_spent` predicate leaf alongside an OZ `spending_limit` primitive
|
|
47
|
+
// covering the same spend semantic - the two adapters never overlap.
|
|
48
|
+
//
|
|
35
49
|
// The default behavior is `deny_all` (OZ context rules are deny-by-default).
|
|
36
50
|
import { identifyProtocol } from "../registry/identify.js";
|
|
37
|
-
/** Compose a PolicyIR from the lowered facts + the resolved scope.
|
|
51
|
+
/** Compose a PolicyIR pair from the lowered facts + the resolved scope.
|
|
38
52
|
* Pure (no randomness, no clock); same inputs -> byte-identical result. */
|
|
39
53
|
export function composeFromRecording(facts, scopeContract, topLevel, opts) {
|
|
54
|
+
const interpreterEnabled = opts.interpreterEnabled === true;
|
|
40
55
|
const ambiguities = [];
|
|
41
56
|
const warnings = [];
|
|
42
|
-
const
|
|
57
|
+
const ozConstraints = [];
|
|
58
|
+
const interpreterConstraints = [];
|
|
43
59
|
const protocol = topLevel
|
|
44
60
|
? identifyProtocol(topLevel.contract, topLevel.fn, topLevel.args, opts.network)
|
|
45
61
|
: null;
|
|
@@ -50,6 +66,11 @@ export function composeFromRecording(facts, scopeContract, topLevel, opts) {
|
|
|
50
66
|
// Outgoing spend -> one spending_limit per spent token. A single caller limit
|
|
51
67
|
// binds only an unambiguous single-token spend; a multi-token flow needs a
|
|
52
68
|
// per-token limit, so each unmatched token surfaces AMOUNT_BOUND_MISSING.
|
|
69
|
+
//
|
|
70
|
+
// Routing: a `window_spent(token, w) <= limit` constraint goes to the OZ IR
|
|
71
|
+
// only when token === scope.contract (OZ's spending_limit binds the
|
|
72
|
+
// CallContract target, not a token parameter). Otherwise it goes to the
|
|
73
|
+
// interpreter IR (the interpreter lowers window_spent to a predicate leaf).
|
|
53
74
|
if (spendTokens.length > 0 && topLevel) {
|
|
54
75
|
let durationFlagged = false;
|
|
55
76
|
for (const token of spendTokens) {
|
|
@@ -75,33 +96,56 @@ export function composeFromRecording(facts, scopeContract, topLevel, opts) {
|
|
|
75
96
|
});
|
|
76
97
|
continue;
|
|
77
98
|
}
|
|
78
|
-
if (windowSeconds !== undefined) {
|
|
79
|
-
|
|
99
|
+
if (windowSeconds !== undefined && limit !== undefined) {
|
|
100
|
+
const spendCond = {
|
|
80
101
|
op: 'compare',
|
|
81
102
|
compare: {
|
|
82
103
|
selector: { kind: 'window_spent', token, windowSeconds },
|
|
83
104
|
operator: 'lte',
|
|
84
105
|
value: limit,
|
|
85
106
|
},
|
|
86
|
-
}
|
|
107
|
+
};
|
|
108
|
+
if (!interpreterEnabled || token === scopeContract) {
|
|
109
|
+
ozConstraints.push(spendCond);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
interpreterConstraints.push(spendCond);
|
|
113
|
+
}
|
|
87
114
|
}
|
|
88
115
|
}
|
|
89
116
|
}
|
|
90
117
|
// Incoming-only / frequency intent: an invocation_count bound is emitted ONLY
|
|
91
118
|
// when the caller supplies the count AND a window - never a fabricated `<= 1`.
|
|
119
|
+
// Routed to the interpreter IR when interpreter is enabled (OZ cannot lower
|
|
120
|
+
// invocation_count); otherwise to the OZ IR (which flags it as uncovered).
|
|
121
|
+
//
|
|
122
|
+
// A recognised swap is NOT an incoming-only flow: it has an outgoing input leg
|
|
123
|
+
// whose spend simply was not attributed to the source account (the
|
|
124
|
+
// fee-sponsored / holder != source case). Its real restrictions - exact path,
|
|
125
|
+
// recipient, and the input-amount cap - come from the protocol-specific pass,
|
|
126
|
+
// so it does NOT get the incoming-only frequency prompt. A caller who wants to
|
|
127
|
+
// rate-limit the swap can still supply an invocationLimit + window, which
|
|
128
|
+
// lowers to an invocation_count for any flow.
|
|
92
129
|
if (spendTokens.length === 0 && topLevel) {
|
|
93
130
|
const invocationLimit = opts.userResponses?.invocationLimit;
|
|
131
|
+
const isRecognisedSwap = protocol?.protocol === 'soroswap';
|
|
94
132
|
if (known && windowSeconds !== undefined && invocationLimit !== undefined) {
|
|
95
|
-
|
|
133
|
+
const icCond = {
|
|
96
134
|
op: 'compare',
|
|
97
135
|
compare: {
|
|
98
136
|
selector: { kind: 'invocation_count', windowSeconds },
|
|
99
137
|
operator: 'lte',
|
|
100
138
|
value: String(invocationLimit),
|
|
101
139
|
},
|
|
102
|
-
}
|
|
140
|
+
};
|
|
141
|
+
if (interpreterEnabled) {
|
|
142
|
+
interpreterConstraints.push(icCond);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
ozConstraints.push(icCond);
|
|
146
|
+
}
|
|
103
147
|
}
|
|
104
|
-
else {
|
|
148
|
+
else if (!isRecognisedSwap) {
|
|
105
149
|
ambiguities.push({
|
|
106
150
|
code: 'FREQUENCY_BOUND_MISSING',
|
|
107
151
|
question: 'Incoming-only flow - what max invocations per window should the policy enforce?',
|
|
@@ -109,54 +153,201 @@ export function composeFromRecording(facts, scopeContract, topLevel, opts) {
|
|
|
109
153
|
warnings.push('frequency bound needed for the incoming-only flow (needs the interpreter predicate); no invocation cap inferred');
|
|
110
154
|
}
|
|
111
155
|
}
|
|
156
|
+
// Per-asset oracle-price bound(s) supplied by the caller -> one
|
|
157
|
+
// oracle_price compare per entry. Routed to the interpreter IR when enabled;
|
|
158
|
+
// otherwise to the OZ IR (which flags it as uncovered).
|
|
159
|
+
const oracleBounds = opts.userResponses?.oraclePriceBound;
|
|
160
|
+
if (oracleBounds) {
|
|
161
|
+
for (const b of oracleBounds) {
|
|
162
|
+
const oracleCond = {
|
|
163
|
+
op: 'compare',
|
|
164
|
+
compare: {
|
|
165
|
+
selector: { kind: 'oracle_price', asset: b.asset },
|
|
166
|
+
operator: b.operator,
|
|
167
|
+
value: b.value,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
if (interpreterEnabled) {
|
|
171
|
+
interpreterConstraints.push(oracleCond);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
ozConstraints.push(oracleCond);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
112
178
|
// Observed recipient allowlist (SEP-41) is a real, recorded constraint the OZ
|
|
113
|
-
// adapter flags as not covered
|
|
179
|
+
// adapter flags as not covered; the interpreter adapter lowers it to an `in`
|
|
180
|
+
// predicate. Unknown protocols emit nothing here. SoroSwap's swap recipient
|
|
181
|
+
// (arg[3]) is the source-of-truth for the swapRecipientAllowlist surface.
|
|
114
182
|
if (topLevel && protocol !== null) {
|
|
115
|
-
|
|
183
|
+
// A SoroSwap input-amount cap binds the caller's limitAmount to call_arg[0]
|
|
184
|
+
// (the exact amount_in) ONLY when no cumulative outgoing spend was detected
|
|
185
|
+
// for the source account - i.e. the input token never moved FROM the source
|
|
186
|
+
// (fee-sponsored swaps, or a holder != source). When a spend WAS detected,
|
|
187
|
+
// the window_spent path above already consumed the limit, so the per-call
|
|
188
|
+
// arg cap is skipped to avoid binding one limit to two different semantics.
|
|
189
|
+
const swapInputAmountCap = spendTokens.length === 0 ? limitAmount : undefined;
|
|
190
|
+
appendProtocolSpecificConstraints(ozConstraints, interpreterConstraints, warnings, ambiguities, facts, topLevel, protocol, opts.userResponses?.swapRecipientAllowlist, swapInputAmountCap, interpreterEnabled);
|
|
116
191
|
}
|
|
192
|
+
// `scope.method` is carried on BOTH IRs so each adapter produces a
|
|
193
|
+
// self-consistent rule. The interpreter adapter lowers scope.method into a
|
|
194
|
+
// `call_fn == <method>` predicate leaf (a real restriction); the OZ adapter
|
|
195
|
+
// flags it as uncovered (CallContract alone permits any method on the
|
|
196
|
+
// contract).
|
|
117
197
|
const scope = { contract: scopeContract };
|
|
118
198
|
if (topLevel?.fn)
|
|
119
199
|
scope.method = topLevel.fn;
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
200
|
+
const buildRule = (constraints) => {
|
|
201
|
+
const rule = {
|
|
202
|
+
roles: [],
|
|
203
|
+
scope: { ...scope },
|
|
204
|
+
constraints,
|
|
205
|
+
};
|
|
206
|
+
if (opts.userResponses?.validUntilLedger !== undefined) {
|
|
207
|
+
rule.expiry = { validUntilLedger: opts.userResponses.validUntilLedger };
|
|
208
|
+
}
|
|
209
|
+
return rule;
|
|
124
210
|
};
|
|
125
|
-
if (opts.userResponses?.validUntilLedger !== undefined) {
|
|
126
|
-
rule.expiry = { validUntilLedger: opts.userResponses.validUntilLedger };
|
|
127
|
-
}
|
|
128
211
|
const ir = {
|
|
129
212
|
chain: 'stellar',
|
|
130
213
|
defaultBehavior: 'deny_all',
|
|
131
|
-
rules: [
|
|
214
|
+
rules: [buildRule(ozConstraints)],
|
|
132
215
|
};
|
|
133
|
-
|
|
216
|
+
const interpreterIr = {
|
|
217
|
+
chain: 'stellar',
|
|
218
|
+
defaultBehavior: 'deny_all',
|
|
219
|
+
rules: [buildRule(interpreterConstraints)],
|
|
220
|
+
};
|
|
221
|
+
return { ir, interpreterIr, ambiguities, warnings };
|
|
134
222
|
}
|
|
135
223
|
/** Add the constraints justified by the recording that the OZ backend cannot
|
|
136
|
-
* express natively
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
|
|
224
|
+
* express natively. Each constraint is routed to either `ozConstraints` (the
|
|
225
|
+
* OZ adapter lowers it) or `interpreterConstraints` (the interpreter adapter
|
|
226
|
+
* lowers it). When `interpreterEnabled` is false, every protocol-specific
|
|
227
|
+
* constraint is routed to `ozConstraints` (which flags it as uncovered) so
|
|
228
|
+
* callers who haven't opted in keep today's warning-driven behaviour.
|
|
229
|
+
* SoroSwap's slippage / oracle / exact-path needs come from `userResponses`
|
|
230
|
+
* (oraclePriceBound + limitAmount) + the recorded path (eq_seq on
|
|
231
|
+
* call_arg[2]). */
|
|
232
|
+
function appendProtocolSpecificConstraints(ozConstraints, interpreterConstraints, warnings, ambiguities, facts, topLevel, protocol, swapRecipientAllowlist, swapInputAmountCap, interpreterEnabled) {
|
|
140
233
|
// SEP-41 transfer / mint: the `to` arg (index 1) is the recipient. Emit it as
|
|
141
|
-
// a single-element allowlist; the
|
|
234
|
+
// a single-element allowlist; the interpreter adapter lowers it to `in`.
|
|
235
|
+
// When interpreter is not enabled, route to OZ so the caller sees today's
|
|
236
|
+
// `value allowlist on arg 1 (arg allowlist)` warning.
|
|
142
237
|
if (protocol.protocol === 'sep41' && (protocol.fn === 'transfer' || protocol.fn === 'mint')) {
|
|
143
238
|
const toArg = topLevel.args[1];
|
|
144
239
|
if (toArg && toArg.type === 'address') {
|
|
145
|
-
|
|
240
|
+
const cond = {
|
|
146
241
|
op: 'in',
|
|
147
242
|
selector: { kind: 'arg', argIndex: 1, scalarType: 'address' },
|
|
148
243
|
values: [toArg.value],
|
|
149
|
-
}
|
|
244
|
+
};
|
|
245
|
+
if (interpreterEnabled) {
|
|
246
|
+
interpreterConstraints.push(cond);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
ozConstraints.push(cond);
|
|
250
|
+
}
|
|
150
251
|
}
|
|
151
252
|
}
|
|
152
|
-
// SoroSwap: the
|
|
153
|
-
//
|
|
154
|
-
// (
|
|
155
|
-
//
|
|
253
|
+
// SoroSwap: the recorded hop path -> eq_seq on the path arg (call_arg[2]).
|
|
254
|
+
// The interpreter adapter is the ONLY way to express an exact ordered
|
|
255
|
+
// sequence (OZ built-ins cannot). Element order is preserved verbatim.
|
|
256
|
+
// When multiple paths were recorded, the intersection is emitted as the
|
|
257
|
+
// canonical single path only if all observations agree; otherwise each is
|
|
258
|
+
// surfaced descriptively.
|
|
156
259
|
if (protocol.protocol === 'soroswap') {
|
|
157
260
|
const paths = facts.allowedPaths?.[topLevel.contract];
|
|
158
261
|
const route = paths?.length === 1 ? paths[0] : undefined;
|
|
159
|
-
|
|
160
|
-
|
|
262
|
+
if (route && route.length > 0 && interpreterEnabled) {
|
|
263
|
+
const pathArgIndex = 2; // SoroSwap swap_exact_tokens_for_tokens: args = [amount_in, amount_out_min, path, to, deadline]
|
|
264
|
+
interpreterConstraints.push({
|
|
265
|
+
op: 'eq_seq',
|
|
266
|
+
selector: { kind: 'arg', argIndex: pathArgIndex, scalarType: 'address' },
|
|
267
|
+
values: route,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
const pathText = route && route.length > 0 ? `; observed path: ${route.join(' -> ')}` : '';
|
|
272
|
+
warnings.push(`SoroSwap swap: slippage / oracle price bound and exact hop path need the interpreter predicate${pathText}`);
|
|
273
|
+
}
|
|
274
|
+
// Input-amount cap: bind the caller's limitAmount to the swap's input-amount
|
|
275
|
+
// argument as `call_arg[i] <= limit`. The index is function-specific -
|
|
276
|
+
// SoroSwap has three swap entrypoints: `swap_exact_tokens_for_tokens` and
|
|
277
|
+
// `swap_exact_in_for_tokens` take the exact input as arg[0], while
|
|
278
|
+
// `swap_tokens_for_exact_tokens` takes the MAXIMUM input (`amount_in_max`)
|
|
279
|
+
// as arg[1] (its arg[0] is the exact OUTPUT, so binding arg[0] there would
|
|
280
|
+
// cap the wrong value and leave the input unbounded). This is a per-call cap
|
|
281
|
+
// that does NOT depend on attributing a token movement to the source account
|
|
282
|
+
// (the fee-sponsored / holder != source case, where the window_spent path
|
|
283
|
+
// detects no spend). Fail-closed: it only ever restricts the permitted input.
|
|
284
|
+
// Routed to the interpreter predicate (OZ built-ins cannot express a per-arg
|
|
285
|
+
// i128 bound); when the interpreter is not enabled it goes to OZ, which flags
|
|
286
|
+
// it uncovered (a warning). The `type === 'i128'` check is defense in depth -
|
|
287
|
+
// protocol identification's argsMatchAbi already pins the input arg to i128.
|
|
288
|
+
const inputArgIndex = soroswapInputAmountArgIndex(protocol.fn);
|
|
289
|
+
const inputAmountArg = inputArgIndex !== undefined ? topLevel.args[inputArgIndex] : undefined;
|
|
290
|
+
if (swapInputAmountCap !== undefined &&
|
|
291
|
+
inputArgIndex !== undefined &&
|
|
292
|
+
inputAmountArg &&
|
|
293
|
+
inputAmountArg.type === 'i128') {
|
|
294
|
+
const cond = {
|
|
295
|
+
op: 'compare',
|
|
296
|
+
compare: {
|
|
297
|
+
selector: { kind: 'arg', argIndex: inputArgIndex, scalarType: 'i128' },
|
|
298
|
+
operator: 'lte',
|
|
299
|
+
value: swapInputAmountCap,
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
if (interpreterEnabled) {
|
|
303
|
+
interpreterConstraints.push(cond);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
ozConstraints.push(cond);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Swap recipient (call_arg[3]): when the caller supplies
|
|
310
|
+
// swapRecipientAllowlist, emit it as an `in` constraint on the recipient
|
|
311
|
+
// arg. When absent, surface RECIPIENT_ALLOWLIST_EMPTY so the caller is
|
|
312
|
+
// prompted; proceeding without an allowlist leaves the recipient
|
|
313
|
+
// unconstrained in the predicate. The ambiguity is only surfaced when
|
|
314
|
+
// the interpreter is enabled - if it isn't, the swap recipient is just
|
|
315
|
+
// ignored (today's behaviour).
|
|
316
|
+
const recipientArg = topLevel.args[3];
|
|
317
|
+
if (swapRecipientAllowlist && swapRecipientAllowlist.length > 0) {
|
|
318
|
+
const cond = {
|
|
319
|
+
op: 'in',
|
|
320
|
+
selector: { kind: 'arg', argIndex: 3, scalarType: 'address' },
|
|
321
|
+
values: [...swapRecipientAllowlist],
|
|
322
|
+
};
|
|
323
|
+
if (interpreterEnabled) {
|
|
324
|
+
interpreterConstraints.push(cond);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
ozConstraints.push(cond);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else if (interpreterEnabled && recipientArg && recipientArg.type === 'address') {
|
|
331
|
+
ambiguities.push({
|
|
332
|
+
code: 'RECIPIENT_ALLOWLIST_EMPTY',
|
|
333
|
+
question: `Recording shows the swap sending to ${recipientArg.value}. What recipient allowlist should the interpreter predicate bind? (omit to leave recipient unconstrained)`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** Positional index of the input-amount argument for a recognized SoroSwap swap
|
|
339
|
+
* function. `swap_exact_tokens_for_tokens` and `swap_exact_in_for_tokens` take
|
|
340
|
+
* the exact input as arg[0]; `swap_tokens_for_exact_tokens` takes the maximum
|
|
341
|
+
* input (`amount_in_max`) as arg[1] - its arg[0] is the exact OUTPUT. Any other
|
|
342
|
+
* function has no positional input-amount argument -> undefined (no cap bound). */
|
|
343
|
+
function soroswapInputAmountArgIndex(fn) {
|
|
344
|
+
switch (fn) {
|
|
345
|
+
case 'swap_exact_tokens_for_tokens':
|
|
346
|
+
case 'swap_exact_in_for_tokens':
|
|
347
|
+
return 0;
|
|
348
|
+
case 'swap_tokens_for_exact_tokens':
|
|
349
|
+
return 1;
|
|
350
|
+
default:
|
|
351
|
+
return undefined;
|
|
161
352
|
}
|
|
162
353
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PredicateNode } from '../types.ts';
|
|
2
|
+
import type { EvalContext } from './evaluate.ts';
|
|
3
|
+
export interface DenyCase {
|
|
4
|
+
dimension: string;
|
|
5
|
+
ctx: EvalContext;
|
|
6
|
+
}
|
|
7
|
+
export interface GeneratedCases {
|
|
8
|
+
permit: EvalContext;
|
|
9
|
+
denies: DenyCase[];
|
|
10
|
+
}
|
|
11
|
+
/** Build deterministic model-evaluated alternatives without mutating the intended call. */
|
|
12
|
+
export declare function generateCases(predicate: PredicateNode, permitCtx: EvalContext): GeneratedCases;
|