@crediolabs/policy-synth 0.1.11 → 0.1.13
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/dist/adapters/interpreter/adapter.js +21 -0
- package/dist/adapters/oz/adapter.js +4 -0
- package/dist/install/build-install-policy.d.ts +218 -0
- package/dist/install/build-install-policy.js +427 -0
- package/dist/install/get-interpreter-info.d.ts +29 -0
- package/dist/install/get-interpreter-info.js +41 -0
- package/dist/ir/types.d.ts +19 -0
- package/dist/predicate/index.d.ts +1 -1
- package/dist/predicate/index.js +1 -1
- package/dist/review-card/builder.js +32 -0
- package/dist/review-card/cross-check.js +20 -0
- package/dist/run/index.d.ts +49 -4
- package/dist/run/index.js +324 -4
- package/dist/run/schemas.d.ts +1500 -4
- package/dist/run/schemas.js +288 -0
- package/dist/synth/compose-from-recording.d.ts +14 -0
- package/dist/synth/compose-from-recording.js +41 -2
- package/dist-cjs/adapters/interpreter/adapter.js +21 -0
- package/dist-cjs/adapters/oz/adapter.js +4 -0
- package/dist-cjs/install/build-install-policy.d.ts +218 -0
- package/dist-cjs/install/build-install-policy.js +431 -0
- package/dist-cjs/install/get-interpreter-info.d.ts +29 -0
- package/dist-cjs/install/get-interpreter-info.js +44 -0
- package/dist-cjs/ir/types.d.ts +19 -0
- package/dist-cjs/predicate/index.d.ts +1 -1
- package/dist-cjs/predicate/index.js +3 -3
- package/dist-cjs/review-card/builder.js +32 -0
- package/dist-cjs/review-card/cross-check.js +20 -0
- package/dist-cjs/run/index.d.ts +49 -4
- package/dist-cjs/run/index.js +339 -3
- package/dist-cjs/run/schemas.d.ts +1500 -4
- package/dist-cjs/run/schemas.js +289 -1
- package/dist-cjs/synth/compose-from-recording.d.ts +14 -0
- package/dist-cjs/synth/compose-from-recording.js +41 -2
- package/package.json +1 -1
- package/src/adapters/interpreter/adapter.ts +21 -0
- package/src/adapters/oz/adapter.ts +4 -0
- package/src/install/build-install-policy.ts +686 -0
- package/src/install/get-interpreter-info.ts +68 -0
- package/src/ir/types.ts +19 -0
- package/src/predicate/index.ts +1 -1
- package/src/review-card/builder.ts +27 -0
- package/src/review-card/cross-check.ts +25 -1
- package/src/run/index.ts +405 -5
- package/src/run/schemas.ts +326 -0
- package/src/synth/compose-from-recording.ts +55 -0
- package/src/synth/evaluate.ts +0 -1
- package/src/synth/permit-context.ts +0 -1
|
@@ -218,6 +218,9 @@ function lowerRule(rule, config) {
|
|
|
218
218
|
* by `lowerRule` to populate `uncovered` before lowering. */
|
|
219
219
|
function unsupportedConstruct(cond) {
|
|
220
220
|
switch (cond.op) {
|
|
221
|
+
// Expressible: `call_arg_scaled` is exactly this construct.
|
|
222
|
+
case 'slippage_floor':
|
|
223
|
+
return null;
|
|
221
224
|
case 'in':
|
|
222
225
|
// The oracle position rule is a hard error and must be raised before a
|
|
223
226
|
// construct can be written off as uncovered - otherwise a subtree that
|
|
@@ -281,6 +284,20 @@ function unsourceableSelector(s) {
|
|
|
281
284
|
* address -> SCOPE_SELF_CALL). */
|
|
282
285
|
function lowerCondition(cond, config) {
|
|
283
286
|
switch (cond.op) {
|
|
287
|
+
// `out >= in * num/den`. The contract refuses den == 0 or a non-positive
|
|
288
|
+
// ratio at install, so a malformed floor fails loudly rather than
|
|
289
|
+
// silently inverting the comparison.
|
|
290
|
+
case 'slippage_floor':
|
|
291
|
+
return {
|
|
292
|
+
op: 'gte',
|
|
293
|
+
left: { kind: 'call_arg', index: cond.outArgIndex },
|
|
294
|
+
right: {
|
|
295
|
+
kind: 'call_arg_scaled',
|
|
296
|
+
index: cond.inArgIndex,
|
|
297
|
+
num: cond.num,
|
|
298
|
+
den: cond.den,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
284
301
|
case 'and':
|
|
285
302
|
return { op: 'and', children: cond.children.map((c) => lowerCondition(c, config)) };
|
|
286
303
|
case 'or':
|
|
@@ -458,6 +475,8 @@ function assertNoOracleDescendants(node) {
|
|
|
458
475
|
}
|
|
459
476
|
function containsOracle(node) {
|
|
460
477
|
switch (node.op) {
|
|
478
|
+
case 'slippage_floor':
|
|
479
|
+
return false;
|
|
461
480
|
case 'compare':
|
|
462
481
|
return node.compare.selector.kind === 'oracle_price';
|
|
463
482
|
case 'in':
|
|
@@ -496,6 +515,8 @@ function assertOracleParamsTighten(params) {
|
|
|
496
515
|
* express. Mirrors the OZ adapter's `describeCondition` for parity. */
|
|
497
516
|
function describeCondition(cond) {
|
|
498
517
|
switch (cond.op) {
|
|
518
|
+
case 'slippage_floor':
|
|
519
|
+
return `slippage floor: arg[${cond.outArgIndex}] >= arg[${cond.inArgIndex}] * ${cond.num}/${cond.den}`;
|
|
499
520
|
case 'in':
|
|
500
521
|
return `value allowlist on ${describeSelector(cond.selector)} (predicate DSL)`;
|
|
501
522
|
case 'eq_seq':
|
|
@@ -207,6 +207,10 @@ function matchSpendingLimit(c) {
|
|
|
207
207
|
* cannot express, used to populate `uncovered`. */
|
|
208
208
|
function describeCondition(cond) {
|
|
209
209
|
switch (cond.op) {
|
|
210
|
+
case 'slippage_floor':
|
|
211
|
+
// The OZ primitives bound a value against a constant; this bounds one
|
|
212
|
+
// call argument against another, which none of them can express.
|
|
213
|
+
return `slippage floor on arg[${cond.outArgIndex}] (OZ built-ins cannot bound one argument against another)`;
|
|
210
214
|
case 'in':
|
|
211
215
|
return `value allowlist on ${describeSelector(cond.selector)} (arg allowlist)`;
|
|
212
216
|
case 'eq_seq':
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { Contract, rpc, type Transaction } from '@stellar/stellar-sdk';
|
|
2
|
+
import { DEFAULT_GRAMMAR_VERSION } from './build-add-context-rule.ts';
|
|
3
|
+
/** Minimal Soroban RPC surface the install pipeline needs. Test seams
|
|
4
|
+
* inject a stub so the builder stays deterministic; production builds
|
|
5
|
+
* the real one via `createRpcServer` from `../record/rpc.ts`. */
|
|
6
|
+
export interface InstallRpcClient {
|
|
7
|
+
getAccount(address: string): Promise<{
|
|
8
|
+
sequenceNumber(): string;
|
|
9
|
+
}>;
|
|
10
|
+
simulateTransaction(tx: Transaction | Parameters<rpc.Server['simulateTransaction']>[0]): Promise<rpc.Api.SimulateTransactionResponse>;
|
|
11
|
+
getLatestLedger(): Promise<{
|
|
12
|
+
sequence: number;
|
|
13
|
+
}>;
|
|
14
|
+
/** Live `grammar_version()` lookup. Returns the deployed u32. */
|
|
15
|
+
getContractVersion(address: string): Promise<number>;
|
|
16
|
+
}
|
|
17
|
+
/** Convert a real rpc.Server into the InstallRpcClient surface. The
|
|
18
|
+
* `getContractVersion` lookup uses `simulateTransaction` against
|
|
19
|
+
* `contract.call('grammar_version')` and decodes the returned u32.
|
|
20
|
+
*
|
|
21
|
+
* The passphrase is a REQUIRED argument rather than something read off the
|
|
22
|
+
* server: `rpc.Server` does not carry one, so reaching for
|
|
23
|
+
* `server.networkPassphrase` yielded a non-string and every version probe
|
|
24
|
+
* died with "Invalid passphrase provided to Transaction". The caller knows
|
|
25
|
+
* which network it dialled; make it say so. */
|
|
26
|
+
export declare function rpcClientFromServer(server: rpc.Server, networkPassphrase: string): InstallRpcClient;
|
|
27
|
+
/** Inputs for the install-policy build. */
|
|
28
|
+
export interface BuildInstallPolicyArgs {
|
|
29
|
+
/** The smart account contract address (C...). */
|
|
30
|
+
smartAccount: string;
|
|
31
|
+
/** The signer that authorises the install (G... wallet). */
|
|
32
|
+
sourceAccount: string;
|
|
33
|
+
/** Network passphrase - pins the hash the auth digest binds. */
|
|
34
|
+
networkPassphrase: string;
|
|
35
|
+
/** The new rule to install. Mirrors the core `ContextRuleDraft`. */
|
|
36
|
+
rule: BuildInstallPolicyRuleDraft;
|
|
37
|
+
/** Per-rule install nonce; 1 for a fresh install. */
|
|
38
|
+
installNonce: number;
|
|
39
|
+
/** Already-encoded (base64) canonical ScVal of the predicate. */
|
|
40
|
+
encodedPredicate: string;
|
|
41
|
+
/** Hex sha256 of the canonical predicate XDR bytes. */
|
|
42
|
+
predicateHash: string;
|
|
43
|
+
/** Per-policy oracle overrides. */
|
|
44
|
+
oracleParams?: {
|
|
45
|
+
maxStalenessSeconds?: number;
|
|
46
|
+
maxDeviationBps?: number;
|
|
47
|
+
maxCrossFeedDeviationBps?: number;
|
|
48
|
+
};
|
|
49
|
+
/** Grammar version override; defaults to 1. */
|
|
50
|
+
grammarVersion?: number;
|
|
51
|
+
/** Base fee in stroops; defaults to BASE_FEE (100). */
|
|
52
|
+
baseFee?: number;
|
|
53
|
+
/** RPC client to use for the simulation pass; injected for tests. */
|
|
54
|
+
rpc: InstallRpcClient;
|
|
55
|
+
/** Ledger window (in ledgers) for the auth entry's `validUntil`. */
|
|
56
|
+
authValidUntilLedgers?: number;
|
|
57
|
+
}
|
|
58
|
+
export type BuildInstallPolicyRuleDraft = {
|
|
59
|
+
contextRuleType: {
|
|
60
|
+
kind: 'default';
|
|
61
|
+
} | {
|
|
62
|
+
kind: 'call_contract';
|
|
63
|
+
contract: string;
|
|
64
|
+
} | {
|
|
65
|
+
kind: 'create_contract';
|
|
66
|
+
wasmHash: string;
|
|
67
|
+
};
|
|
68
|
+
name: string;
|
|
69
|
+
validUntilLedger: number | null;
|
|
70
|
+
signers: BuildInstallPolicySignerDraft[];
|
|
71
|
+
policies: BuildInstallPolicyPolicyRef[];
|
|
72
|
+
};
|
|
73
|
+
export type BuildInstallPolicySignerDraft = {
|
|
74
|
+
kind: 'delegated';
|
|
75
|
+
address: string;
|
|
76
|
+
} | {
|
|
77
|
+
kind: 'external';
|
|
78
|
+
verifier: string;
|
|
79
|
+
keyBytes: string;
|
|
80
|
+
};
|
|
81
|
+
export type BuildInstallPolicyPolicyRef = {
|
|
82
|
+
kind: 'interpreter';
|
|
83
|
+
interpreterAddress: string;
|
|
84
|
+
predicateBlobBase64: string;
|
|
85
|
+
} | {
|
|
86
|
+
kind: 'oz_builtin';
|
|
87
|
+
instanceAddress: string;
|
|
88
|
+
primitive: {
|
|
89
|
+
primitive: 'spending_limit' | 'simple_threshold' | 'weighted_threshold';
|
|
90
|
+
params: Record<string, unknown>;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
/** Human-readable description of the install call, decoded FROM the built
|
|
94
|
+
* XDR (not from the input args). The XDR is the source of truth: a
|
|
95
|
+
* human approving a review card needs the description to mirror the bytes
|
|
96
|
+
* the wallet will sign. Deriving `describes` from the input args would
|
|
97
|
+
* re-describe the caller's intent rather than the transaction. */
|
|
98
|
+
export interface InstallCallDescribes {
|
|
99
|
+
/** Smart account that owns the new rule (echoed from the XDR). */
|
|
100
|
+
targetContract: string;
|
|
101
|
+
/** The fn name on the target (always `add_context_rule` today). */
|
|
102
|
+
fnName: 'add_context_rule';
|
|
103
|
+
/** The new rule's display name, decoded from args[1]. */
|
|
104
|
+
ruleName: string;
|
|
105
|
+
/** The `validUntil` ledger sequence decoded from args[2] (null when void). */
|
|
106
|
+
validUntilLedger: number | null;
|
|
107
|
+
/** The signers attached to the rule, decoded from args[3]. The
|
|
108
|
+
* `verifier`/`keyBytes` payload of an external signer is omitted - it
|
|
109
|
+
* is not material to the review card and stays opaque to keep the
|
|
110
|
+
* description focused on what the human has to recognise. */
|
|
111
|
+
signers: Array<{
|
|
112
|
+
kind: 'delegated';
|
|
113
|
+
address: string;
|
|
114
|
+
} | {
|
|
115
|
+
kind: 'external';
|
|
116
|
+
verifier: string;
|
|
117
|
+
}>;
|
|
118
|
+
/** One entry per policy attached to the rule, decoded from the policies
|
|
119
|
+
* map (args[4]). The address is the map key; the kind + extras below
|
|
120
|
+
* describe the value. The interpreter policy also reports the
|
|
121
|
+
* sha256 of the predicate blob actually embedded in the XDR - so a
|
|
122
|
+
* mismatch between the wire bytes and the review card is detectable
|
|
123
|
+
* by reading `describes`. */
|
|
124
|
+
policies: Array<{
|
|
125
|
+
kind: 'interpreter';
|
|
126
|
+
address: string;
|
|
127
|
+
installNonce: number;
|
|
128
|
+
predicateHash: string;
|
|
129
|
+
predicateSha256OfEmbeddedBytes: string;
|
|
130
|
+
} | {
|
|
131
|
+
kind: 'oz_builtin';
|
|
132
|
+
address: string;
|
|
133
|
+
primitive: 'spending_limit' | 'simple_threshold' | 'weighted_threshold';
|
|
134
|
+
}>;
|
|
135
|
+
/** The install nonce, decoded from the interpreter policy's
|
|
136
|
+
* `install_nonce` field. Echoed at the top level for reviewer convenience;
|
|
137
|
+
* the per-policy entry is the source of truth. */
|
|
138
|
+
installNonce: number;
|
|
139
|
+
}
|
|
140
|
+
/** Output of the install-policy build. The unsigned XDR is the wallet's
|
|
141
|
+
* input; the captured auth nonce + invocation root make the response
|
|
142
|
+
* self-describing for callers that want to inspect what they signed. */
|
|
143
|
+
export interface BuildInstallPolicyResult {
|
|
144
|
+
/** Unsigned Soroban transaction envelope, base64 XDR. */
|
|
145
|
+
unsignedXdr: string;
|
|
146
|
+
/** Smart account contract address (echo). */
|
|
147
|
+
smartAccount: string;
|
|
148
|
+
/** Source account (echo) - the address that must sign. */
|
|
149
|
+
sourceAccount: string;
|
|
150
|
+
/** The host-call target + fn name. Call 2 (interpreter.install) is NOT
|
|
151
|
+
* emitted here; the account assigns the rule id in call 1 and call 2
|
|
152
|
+
* needs that id to bind its `context_rule_ids`. See `followUp` below. */
|
|
153
|
+
call: {
|
|
154
|
+
contract: string;
|
|
155
|
+
fn: 'add_context_rule';
|
|
156
|
+
};
|
|
157
|
+
/** The follow-up call the caller MUST issue after this one confirms.
|
|
158
|
+
* The rule id the account assigns in call 1 is required to bind the
|
|
159
|
+
* interpreter's `install` auth context. */
|
|
160
|
+
followUp: {
|
|
161
|
+
contract: 'interpreter';
|
|
162
|
+
fn: 'install';
|
|
163
|
+
requiresRuleIdFromCallOne: true;
|
|
164
|
+
hint: string;
|
|
165
|
+
};
|
|
166
|
+
/** Human-readable description of the install call, decoded FROM the
|
|
167
|
+
* built unsigned XDR (not from the input args). The wallet signature
|
|
168
|
+
* binds to bytes; the review card has to bind to the same bytes, so
|
|
169
|
+
* this is the only safe source. */
|
|
170
|
+
describes: InstallCallDescribes;
|
|
171
|
+
/** The auth nonce the host assigned to this call (snapshot). */
|
|
172
|
+
authNonce: string;
|
|
173
|
+
/** The ledger sequence + window the auth entry expires at. */
|
|
174
|
+
authValidUntilLedger: number;
|
|
175
|
+
/** The captured rootInvocation so a downstream caller can verify the
|
|
176
|
+
* signature payload matches the one the host expects. */
|
|
177
|
+
rootInvocationXdr: string;
|
|
178
|
+
}
|
|
179
|
+
/** Build the unsigned transaction envelope for `account.add_context_rule(...)`.
|
|
180
|
+
* The output XDR is signed by the wallet, not by us. */
|
|
181
|
+
export declare function buildInstallPolicyXdr(args: BuildInstallPolicyArgs): Promise<BuildInstallPolicyResult>;
|
|
182
|
+
/** Build an unsigned XDR for `account.remove_context_rule(ruleId)`. The
|
|
183
|
+
* smart account itself handles uninstalling each attached policy (calling
|
|
184
|
+
* `interpreter.uninstall` for the interpreter policy); this builder only
|
|
185
|
+
* emits the account-side removal call.
|
|
186
|
+
*
|
|
187
|
+
* Who may authorise it is the ACCOUNT's decision, and the account's source is
|
|
188
|
+
* not in this repo. The interpreter's own `uninstall` is master-gated, but
|
|
189
|
+
* that is a different entry point from this one, so do not restate it as the
|
|
190
|
+
* rule for this call. What is proven on testnet is that the account's
|
|
191
|
+
* deployer can revoke. This builder does not pre-check the signer: it would
|
|
192
|
+
* be guessing at a contract it cannot read, and the chain is the authority. */
|
|
193
|
+
export declare function buildRevokePolicyXdr(args: {
|
|
194
|
+
smartAccount: string;
|
|
195
|
+
sourceAccount: string;
|
|
196
|
+
ruleId: number;
|
|
197
|
+
networkPassphrase: string;
|
|
198
|
+
rpc: InstallRpcClient;
|
|
199
|
+
baseFee?: number;
|
|
200
|
+
authValidUntilLedgers?: number;
|
|
201
|
+
}): Promise<BuildRevokePolicyResult>;
|
|
202
|
+
export interface BuildRevokePolicyResult {
|
|
203
|
+
unsignedXdr: string;
|
|
204
|
+
smartAccount: string;
|
|
205
|
+
sourceAccount: string;
|
|
206
|
+
call: {
|
|
207
|
+
contract: string;
|
|
208
|
+
fn: 'remove_context_rule';
|
|
209
|
+
ruleId: number;
|
|
210
|
+
};
|
|
211
|
+
authNonce: string;
|
|
212
|
+
authValidUntilLedger: number;
|
|
213
|
+
rootInvocationXdr: string;
|
|
214
|
+
}
|
|
215
|
+
/** Re-export so the run-layer does not need to import from
|
|
216
|
+
* build-add-context-rule.ts (keeps the install/ -> install/ dependency
|
|
217
|
+
* direction intact). */
|
|
218
|
+
export { Contract, DEFAULT_GRAMMAR_VERSION };
|