@crediolabs/policy-synth 0.1.9 → 0.1.11
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 +62 -11
- package/dist/errors.d.ts +1 -1
- package/dist/install/build-add-context-rule.d.ts +48 -0
- package/dist/install/build-add-context-rule.js +304 -0
- package/dist/ir/types.d.ts +4 -0
- package/dist/predicate/decode.d.ts +11 -0
- package/dist/predicate/decode.js +234 -0
- package/dist/predicate/encode.js +79 -14
- package/dist/predicate/from-json.d.ts +4 -0
- package/dist/predicate/from-json.js +113 -0
- package/dist/predicate/index.d.ts +2 -0
- package/dist/predicate/index.js +5 -1
- package/dist/registry/identify.js +21 -2
- package/dist/registry/protocols.d.ts +50 -1
- package/dist/registry/protocols.js +88 -0
- package/dist/review-card/builder.d.ts +13 -0
- package/dist/review-card/builder.js +61 -5
- package/dist/review-card/cross-check.js +50 -3
- package/dist/review-card/index.d.ts +1 -1
- package/dist/review-card/index.js +1 -1
- package/dist/run/index.d.ts +8 -2
- package/dist/run/index.js +2 -1
- package/dist/run/schemas.d.ts +16 -4
- package/dist/run/schemas.js +14 -0
- package/dist/synth/compose-from-recording.d.ts +4 -0
- package/dist/synth/compose-from-recording.js +34 -8
- package/dist/synth/deny-cases.d.ts +4 -1
- package/dist/synth/deny-cases.js +3 -1
- package/dist/synth/evaluate.js +130 -3
- package/dist/synth/permit-context.d.ts +15 -0
- package/dist/synth/permit-context.js +116 -0
- package/dist/synth/synthesize-from-mandate.d.ts +17 -2
- package/dist/synth/synthesize-from-mandate.js +27 -2
- package/dist/synth/synthesize-from-recording.d.ts +25 -1
- package/dist/synth/synthesize-from-recording.js +80 -3
- package/dist/types.d.ts +9 -0
- package/dist-cjs/adapters/interpreter/adapter.js +62 -11
- package/dist-cjs/errors.d.ts +1 -1
- package/dist-cjs/install/build-add-context-rule.d.ts +48 -0
- package/dist-cjs/install/build-add-context-rule.js +308 -0
- package/dist-cjs/ir/types.d.ts +4 -0
- package/dist-cjs/predicate/decode.d.ts +11 -0
- package/dist-cjs/predicate/decode.js +239 -0
- package/dist-cjs/predicate/encode.js +79 -14
- package/dist-cjs/predicate/from-json.d.ts +4 -0
- package/dist-cjs/predicate/from-json.js +116 -0
- package/dist-cjs/predicate/index.d.ts +2 -0
- package/dist-cjs/predicate/index.js +10 -2
- package/dist-cjs/registry/identify.js +20 -1
- package/dist-cjs/registry/protocols.d.ts +50 -1
- package/dist-cjs/registry/protocols.js +89 -1
- package/dist-cjs/review-card/builder.d.ts +13 -0
- package/dist-cjs/review-card/builder.js +62 -5
- package/dist-cjs/review-card/cross-check.js +50 -3
- package/dist-cjs/review-card/index.d.ts +1 -1
- package/dist-cjs/review-card/index.js +2 -1
- package/dist-cjs/run/index.d.ts +8 -2
- package/dist-cjs/run/index.js +2 -1
- package/dist-cjs/run/schemas.d.ts +16 -4
- package/dist-cjs/run/schemas.js +14 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +4 -0
- package/dist-cjs/synth/compose-from-recording.js +34 -8
- package/dist-cjs/synth/deny-cases.d.ts +4 -1
- package/dist-cjs/synth/deny-cases.js +3 -0
- package/dist-cjs/synth/evaluate.js +130 -3
- package/dist-cjs/synth/permit-context.d.ts +15 -0
- package/dist-cjs/synth/permit-context.js +119 -0
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +17 -2
- package/dist-cjs/synth/synthesize-from-mandate.js +27 -2
- package/dist-cjs/synth/synthesize-from-recording.d.ts +25 -1
- package/dist-cjs/synth/synthesize-from-recording.js +80 -3
- package/dist-cjs/types.d.ts +9 -0
- package/package.json +5 -2
- package/src/adapters/interpreter/adapter.ts +72 -11
- package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +28 -3
- package/src/errors.ts +13 -0
- package/src/install/build-add-context-rule.ts +429 -0
- package/src/ir/types.ts +4 -0
- package/src/predicate/decode.ts +242 -0
- package/src/predicate/encode.ts +110 -13
- package/src/predicate/from-json.ts +124 -0
- package/src/predicate/index.ts +5 -1
- package/src/registry/identify.ts +22 -2
- package/src/registry/protocols.ts +90 -1
- package/src/review-card/builder.ts +57 -5
- package/src/review-card/cross-check.ts +50 -3
- package/src/review-card/index.ts +1 -0
- package/src/run/index.ts +16 -2
- package/src/run/schemas.ts +14 -0
- package/src/synth/compose-from-recording.ts +46 -7
- package/src/synth/deny-cases.ts +3 -1
- package/src/synth/evaluate.ts +131 -3
- package/src/synth/permit-context.ts +137 -0
- package/src/synth/synthesize-from-mandate.ts +40 -4
- package/src/synth/synthesize-from-recording.ts +116 -6
- package/src/types.ts +12 -0
|
@@ -47,7 +47,10 @@ export const ORACLE_DEFAULTS = {
|
|
|
47
47
|
const CAPABILITIES = {
|
|
48
48
|
supportsSpendWindow: true,
|
|
49
49
|
supportsThreshold: false, // thresholds are the OZ adapter's job
|
|
50
|
-
|
|
50
|
+
// False for the predicate-leaf path: the interpreter refuses a `valid_until`
|
|
51
|
+
// leaf at install. Expiry is still available, but as the context rule's
|
|
52
|
+
// validUntilLedger, which the smart account enforces - not as a predicate.
|
|
53
|
+
supportsTimeExpiry: false,
|
|
51
54
|
supportsOraclePrice: true,
|
|
52
55
|
supportsInvocationCount: true,
|
|
53
56
|
supportsGeneralPredicate: true,
|
|
@@ -216,26 +219,61 @@ function lowerRule(rule, config) {
|
|
|
216
219
|
function unsupportedConstruct(cond) {
|
|
217
220
|
switch (cond.op) {
|
|
218
221
|
case 'in':
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
+
// The oracle position rule is a hard error and must be raised before a
|
|
223
|
+
// construct can be written off as uncovered - otherwise a subtree that
|
|
224
|
+
// is BOTH misplaced-oracle and unsourceable would be silently dropped
|
|
225
|
+
// instead of rejected.
|
|
226
|
+
assertNoOracleDescendants(cond);
|
|
227
|
+
return (unsourceableSelector(cond.selector) ??
|
|
228
|
+
(cond.selector.kind === 'calldata' || cond.selector.kind === 'value'
|
|
229
|
+
? `EVM \`${cond.selector.kind}\` selector on allowlist (predicate DSL)`
|
|
230
|
+
: null));
|
|
222
231
|
case 'eq_seq':
|
|
223
|
-
return cond.selector
|
|
224
|
-
|
|
225
|
-
|
|
232
|
+
return (unsourceableSelector(cond.selector) ??
|
|
233
|
+
(cond.selector.kind === 'calldata' || cond.selector.kind === 'value'
|
|
234
|
+
? `EVM \`${cond.selector.kind}\` selector on ordered-sequence equality (predicate DSL)`
|
|
235
|
+
: null));
|
|
226
236
|
case 'compare': {
|
|
227
237
|
const s = cond.compare.selector;
|
|
228
238
|
if (s.kind === 'calldata')
|
|
229
239
|
return 'EVM calldata comparison (predicate DSL)';
|
|
230
240
|
if (s.kind === 'value')
|
|
231
241
|
return 'tx.value comparison (predicate DSL)';
|
|
232
|
-
|
|
242
|
+
// The on-chain interpreter sees ONE authorized call - there is no
|
|
243
|
+
// `Context.sub_invocations` in v1 - so it cannot observe the
|
|
244
|
+
// transaction's token movements. `amount` has no value to read, and
|
|
245
|
+
// `window_spent` accumulates BY that amount, so its counter would never
|
|
246
|
+
// move. Deriving either from the call payload would quietly swap "value
|
|
247
|
+
// actually moved" for "value the caller declared" - a weaker guarantee
|
|
248
|
+
// than the review card would be claiming.
|
|
249
|
+
//
|
|
250
|
+
// Rolling spend caps belong to the OZ `spending_limit` primitive, which
|
|
251
|
+
// is already audited and which the OZ adapter emits. A per-call cap is
|
|
252
|
+
// expressible here as `arg_field`; bounding it with `invocation_count`
|
|
253
|
+
// gives an enforceable ceiling per window.
|
|
254
|
+
return unsourceableSelector(s);
|
|
233
255
|
}
|
|
256
|
+
// Recurse: a nested `and`/`or`/`not` must not smuggle a selector past the
|
|
257
|
+
// pre-scan, which only sees top-level constraints.
|
|
234
258
|
case 'and':
|
|
259
|
+
return cond.children.map(unsupportedConstruct).find((u) => u !== null) ?? null;
|
|
235
260
|
case 'or':
|
|
261
|
+
assertNoOracleDescendants(cond);
|
|
262
|
+
return cond.children.map(unsupportedConstruct).find((u) => u !== null) ?? null;
|
|
236
263
|
case 'not':
|
|
237
|
-
|
|
264
|
+
assertNoOracleDescendants(cond);
|
|
265
|
+
return unsupportedConstruct(cond.child);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** Selectors whose value the on-chain interpreter has no way to obtain. */
|
|
269
|
+
function unsourceableSelector(s) {
|
|
270
|
+
if (s.kind === 'amount') {
|
|
271
|
+
return `per-call amount comparison on ${s.token} - the interpreter cannot observe token movements; express a per-call cap with arg_field, or a rolling cap with the OZ spending_limit primitive`;
|
|
272
|
+
}
|
|
273
|
+
if (s.kind === 'window_spent') {
|
|
274
|
+
return `rolling spend cap on ${s.token} over ${s.windowSeconds}s - not enforceable by the interpreter; use the OZ spending_limit primitive, or bound per-call value with arg_field plus invocation_count`;
|
|
238
275
|
}
|
|
276
|
+
return null;
|
|
239
277
|
}
|
|
240
278
|
/** Lower one IR condition to a PredicateNode. Enforces the oracle position
|
|
241
279
|
* rule (oracle_price leaves must sit directly under the top-level `and`) and
|
|
@@ -311,10 +349,13 @@ function lowerSelector(s) {
|
|
|
311
349
|
return { kind: 'call_arg_len', index: s.argIndex };
|
|
312
350
|
case 'arg_field':
|
|
313
351
|
return { kind: 'call_arg_field', index: s.argIndex, element: s.element, field: s.field };
|
|
352
|
+
// `amount` / `window_spent` are filtered out by `unsupportedConstruct`
|
|
353
|
+
// before lowering - the interpreter cannot source either on chain. Reaching
|
|
354
|
+
// here means the pre-scan was bypassed, so fail loudly rather than emit a
|
|
355
|
+
// leaf the contract will refuse.
|
|
314
356
|
case 'amount':
|
|
315
|
-
return { kind: 'amount', token: s.token };
|
|
316
357
|
case 'window_spent':
|
|
317
|
-
|
|
358
|
+
throw new Error(`interpreter adapter cannot lower \`${s.kind}\`: it should have been reported as uncovered`);
|
|
318
359
|
case 'invocation_count':
|
|
319
360
|
return { kind: 'invocation_count_in_window', windowSecs: s.windowSeconds };
|
|
320
361
|
case 'now':
|
|
@@ -341,6 +382,16 @@ function lowerSelector(s) {
|
|
|
341
382
|
* - invocation_count -> u32 (counts are small non-negative integers)
|
|
342
383
|
* - now / valid_until -> u64 (unix timestamps in seconds) */
|
|
343
384
|
function literalFromIRCompare(c) {
|
|
385
|
+
// An oracle comparand is not a bare literal: the interpreter refuses one,
|
|
386
|
+
// because assuming it shares the normalised 9-dp basis is what made a raw
|
|
387
|
+
// 14-dp threshold permit everything it was written to deny.
|
|
388
|
+
if (c.selector.kind === 'oracle_price') {
|
|
389
|
+
if (c.valueDecimals === undefined) {
|
|
390
|
+
throw toolError('ORACLE_THRESHOLD_BASIS_REQUIRED', 'An oracle price bound must declare the decimal basis of its threshold ' +
|
|
391
|
+
'(oracle prices normalise to 9 decimals).');
|
|
392
|
+
}
|
|
393
|
+
return { kind: 'oracle_threshold', value: c.value, decimals: c.valueDecimals };
|
|
394
|
+
}
|
|
344
395
|
const scalarType = selectorScalarType(c.selector);
|
|
345
396
|
return literalFromScalar(c.value, scalarType);
|
|
346
397
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ErrorCode = 'RECORDING_FAILED' | 'RECORDING_VALIDATION_FAILED' | 'SCOPE_UNRESOLVED' | 'SYNTHESIS_ERROR' | 'MALFORMED_PREDICATE' | 'SIMULATION_ERROR' | 'VERIFICATION_FAILED' | 'DENY_CASE_FAILURE' | 'PERMIT_CASE_FAILED' | 'SUMMARY_DRIFT' | 'INSTALL_BUILD_FAILED' | 'INSTALL_CONFIRM_MISSING' | 'INSTALL_CONFIRM_EXPIRED' | 'REVOKE_BUILD_FAILED' | 'REVOKE_CONFIRM_MISSING' | 'USER_REJECTED_SIGN' | 'WALLET_TIMEOUT' | 'WALLET_UNAVAILABLE' | 'PREDICATE_TOO_LARGE' | 'PREDICATE_TOO_DEEP' | 'TOO_MANY_LEAVES' | 'IN_OPERAND_LIMIT' | 'PREDICATE_ORACLE_OVER_LIMIT' | 'POLICY_CAP_EXCEEDED' | 'WASM_TOO_LARGE' | 'MASTER_AUTH_REQUIRED' | 'NONCE_REPLAY' | 'VERSION_MISMATCH' | 'ARITHMETIC_OVERFLOW' | 'AMOUNT_OVERFLOW' | 'RULE_SIGNERS_CHANGED' | 'SCOPE_SELF_CALL' | 'ORACLE_STALE' | 'ORACLE_MISSING' | 'ORACLE_DEVIATION_EXCEEDED' | 'ORACLE_MALFORMED_HISTORY' | 'ORACLE_FINGERPRINT_DRIFT' | 'ORACLE_PAUSED' | 'ORACLE_LEAF_INVALID_POSITION' | 'ORACLE_PARAMS_OUT_OF_RANGE' | 'ORACLE_DECIMALS_MISMATCH' | 'COMPILE_OK' | 'COMPILE_GATE_FAILED';
|
|
1
|
+
export type ErrorCode = 'RECORDING_FAILED' | 'RECORDING_VALIDATION_FAILED' | 'SCOPE_UNRESOLVED' | 'SYNTHESIS_ERROR' | 'MALFORMED_PREDICATE' | 'SIMULATION_ERROR' | 'VERIFICATION_FAILED' | 'DENY_CASE_FAILURE' | 'PERMIT_CASE_FAILED' | 'SUMMARY_DRIFT' | 'INSTALL_BUILD_FAILED' | 'INSTALL_CONFIRM_MISSING' | 'INSTALL_CONFIRM_EXPIRED' | 'REVOKE_BUILD_FAILED' | 'REVOKE_CONFIRM_MISSING' | 'USER_REJECTED_SIGN' | 'WALLET_TIMEOUT' | 'WALLET_UNAVAILABLE' | 'PREDICATE_TOO_LARGE' | 'PREDICATE_TOO_DEEP' | 'TOO_MANY_LEAVES' | 'IN_OPERAND_LIMIT' | 'PREDICATE_ORACLE_OVER_LIMIT' | 'POLICY_CAP_EXCEEDED' | 'WASM_TOO_LARGE' | 'MASTER_AUTH_REQUIRED' | 'NONCE_REPLAY' | 'VERSION_MISMATCH' | 'ARITHMETIC_OVERFLOW' | 'AMOUNT_OVERFLOW' | 'RULE_SIGNERS_CHANGED' | 'SCOPE_SELF_CALL' | 'ARG_MISMATCH' | 'CONTRACT_SCOPE' | 'UNSUPPORTED_NODE' | 'STATEFUL_BOUND' | 'NOT_IN_ALLOWLIST' | 'FREQUENCY' | 'SLIPPAGE_FLOOR' | 'ORACLE_STALE' | 'ORACLE_MISSING' | 'ORACLE_NO_CONFIRMATION' | 'ORACLE_DEVIATION_EXCEEDED' | 'ORACLE_MALFORMED_HISTORY' | 'ORACLE_FINGERPRINT_DRIFT' | 'ORACLE_PAUSED' | 'ORACLE_LEAF_INVALID_POSITION' | 'ORACLE_PARAMS_OUT_OF_RANGE' | 'ORACLE_DECIMALS_MISMATCH' | 'ORACLE_THRESHOLD_DECIMALS_OUT_OF_RANGE' | 'ORACLE_THRESHOLD_BASIS_REQUIRED' | 'COMPILE_OK' | 'COMPILE_GATE_FAILED';
|
|
2
2
|
export interface ToolError {
|
|
3
3
|
code: ErrorCode;
|
|
4
4
|
message: string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { xdr } from '@stellar/stellar-sdk';
|
|
2
|
+
import { type ContextRuleDraft, type PolicyRef, type SignerDraft } from '../types.ts';
|
|
3
|
+
/** Inputs for `buildAddContextRuleArgs`. The fields are the ones the contract
|
|
4
|
+
* validates at install; everything else is downstream. */
|
|
5
|
+
export interface BuildAddContextRuleArgs {
|
|
6
|
+
/** The auth-context rule signers to attach - `Vec<Signer>` wire form.
|
|
7
|
+
* These are the SIGNERS the new rule authorises, not the deployer's
|
|
8
|
+
* admin session. The deployer signs the install call via the admin rule
|
|
9
|
+
* already on the account, which is separate from this list. */
|
|
10
|
+
signers: SignerDraft[];
|
|
11
|
+
/** The policy(ies) to attach to the new rule. Only `interpreter` refs
|
|
12
|
+
* reach the wire today; `oz_builtin` is reserved for Phase 06 follow-ups. */
|
|
13
|
+
policies: PolicyRef[];
|
|
14
|
+
/** Per-policy install nonce; 1 for a fresh install. */
|
|
15
|
+
installNonce: number;
|
|
16
|
+
/** Already-encoded (base64) canonical ScVal of the predicate. The builder
|
|
17
|
+
* decodes it to bytes for the `predicate` field and re-hashes to confirm
|
|
18
|
+
* the caller-supplied `predicateHash` matches. */
|
|
19
|
+
encodedPredicate: string;
|
|
20
|
+
/** Hex sha256 of the canonical predicate XDR bytes. */
|
|
21
|
+
predicateHash: string;
|
|
22
|
+
/** Per-policy oracle overrides. Absent on any key uses the wasm default. */
|
|
23
|
+
oracleParams?: {
|
|
24
|
+
maxStalenessSeconds?: number;
|
|
25
|
+
maxDeviationBps?: number;
|
|
26
|
+
/** Bound between the primary and secondary feeds. Tighten-only against
|
|
27
|
+
* the wasm default, like the other two. */
|
|
28
|
+
maxCrossFeedDeviationBps?: number;
|
|
29
|
+
};
|
|
30
|
+
/** Hard pin to the interpreter's wasm grammar. Refusing here fails closed
|
|
31
|
+
* before the chain does; the contract will refuse a mismatch again. */
|
|
32
|
+
grammarVersion?: number;
|
|
33
|
+
}
|
|
34
|
+
export declare const DEFAULT_GRAMMAR_VERSION: 1;
|
|
35
|
+
/** The verb `add_context_rule` takes on the wire. */
|
|
36
|
+
export declare const ADD_CONTEXT_RULE_SYMBOL: "add_context_rule";
|
|
37
|
+
/** Tuple of `ScVal` arguments to pass to `Operation.invokeHostFunction` for
|
|
38
|
+
* `add_context_rule`. Order matches the OZ trait signature. */
|
|
39
|
+
export type AddContextRuleArgs = readonly [
|
|
40
|
+
contextType: xdr.ScVal,
|
|
41
|
+
name: xdr.ScVal,
|
|
42
|
+
validUntil: xdr.ScVal,
|
|
43
|
+
signers: xdr.ScVal,
|
|
44
|
+
policies: xdr.ScVal
|
|
45
|
+
];
|
|
46
|
+
/** Build the `add_context_rule` invocation args. Throws a `ToolError`-shaped
|
|
47
|
+
* error on limit breaches or malformed input. */
|
|
48
|
+
export declare function buildAddContextRuleArgs(draft: ContextRuleDraft, args: BuildAddContextRuleArgs): AddContextRuleArgs;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// src/install/build-add-context-rule.ts - pure builder for the OZ
|
|
2
|
+
// `add_context_rule` invocation arguments.
|
|
3
|
+
//
|
|
4
|
+
// Hand-rolled instead of reusing the scripts' helpers because the product
|
|
5
|
+
// path (Phase 06 install_policy) needs a single callable that takes a typed
|
|
6
|
+
// `ContextRuleDraft` + predicate bytes and returns the `ScVal` argument list
|
|
7
|
+
// the host will pass to `add_context_rule`. The script-level helpers stay
|
|
8
|
+
// out of `src/` to avoid dragging the ts-node SDK into the library surface.
|
|
9
|
+
//
|
|
10
|
+
// The OZ trait signature is pinned in
|
|
11
|
+
// `stellar-contracts::smart_account::SmartAccount::add_context_rule`:
|
|
12
|
+
//
|
|
13
|
+
// fn add_context_rule(
|
|
14
|
+
// e: &Env,
|
|
15
|
+
// context_type: ContextRuleType,
|
|
16
|
+
// name: String,
|
|
17
|
+
// valid_until: Option<u32>,
|
|
18
|
+
// signers: Vec<Signer>,
|
|
19
|
+
// policies: Map<Address, Val>,
|
|
20
|
+
// ) -> ContextRule
|
|
21
|
+
//
|
|
22
|
+
// The default body calls `e.current_contract_address().require_auth()`, so
|
|
23
|
+
// installation goes through the account's `__check_auth` -> `do_check_auth`.
|
|
24
|
+
// That requires the DEPLOYER to have shipped an admin rule with NO
|
|
25
|
+
// restrictive policy, otherwise the policy would refuse the very call that
|
|
26
|
+
// installs it. The caller is responsible for that contract; this builder
|
|
27
|
+
// just emits the args.
|
|
28
|
+
//
|
|
29
|
+
// Two hard limits the contract enforces (verified against
|
|
30
|
+
// `storage::validate_signers_and_policies` at /tmp/ozsc):
|
|
31
|
+
// - signers.len() <= MAX_SIGNERS (15)
|
|
32
|
+
// - policies.len() <= MAX_POLICIES (5)
|
|
33
|
+
// - signers and policies must not both be empty
|
|
34
|
+
// The same limits are mirrored in `OZ_LIMITS` (see src/types.ts) and
|
|
35
|
+
// checked here fail-closed before any encoding work - the user gets a
|
|
36
|
+
// typed error with the specific cap, not a runtime trap.
|
|
37
|
+
//
|
|
38
|
+
// Policypayloads are `Map<Address, Val>`. The host orders map entries by the
|
|
39
|
+
// symbol STRING of the key, NOT by the key's XDR bytes (a length prefix
|
|
40
|
+
// would otherwise put `amount` before `address` and produce a map the
|
|
41
|
+
// contract reads differently - a previous session lost hours to this). Our
|
|
42
|
+
// only key is an `Address`, so we build a single-entry map and the ordering
|
|
43
|
+
// question is moot, but the comment is here so the next map-bearing code
|
|
44
|
+
// in this file does not regress.
|
|
45
|
+
//
|
|
46
|
+
// Pure: no network, no signing. The caller hands the returned ScVal[] to
|
|
47
|
+
// `Operation.invokeHostFunction` AFTER wiring the auth entries, otherwise
|
|
48
|
+
// the account refuses with `Error(Auth, InvalidAction)`.
|
|
49
|
+
import { createHash } from 'node:crypto';
|
|
50
|
+
import { Address, xdr } from '@stellar/stellar-sdk';
|
|
51
|
+
import { OZ_LIMITS, } from "../types.js";
|
|
52
|
+
export const DEFAULT_GRAMMAR_VERSION = 1;
|
|
53
|
+
/** The verb `add_context_rule` takes on the wire. */
|
|
54
|
+
export const ADD_CONTEXT_RULE_SYMBOL = 'add_context_rule';
|
|
55
|
+
/** Build the `add_context_rule` invocation args. Throws a `ToolError`-shaped
|
|
56
|
+
* error on limit breaches or malformed input. */
|
|
57
|
+
export function buildAddContextRuleArgs(draft, args) {
|
|
58
|
+
// ---- 1. Cap checks (fail-closed before any encoding) ----
|
|
59
|
+
if (args.signers.length > OZ_LIMITS.maxSignersPerRule) {
|
|
60
|
+
throw limitError('INSTALL_BUILD_FAILED', `signers ${args.signers.length} exceed MAX_SIGNERS_PER_RULE ${OZ_LIMITS.maxSignersPerRule}`);
|
|
61
|
+
}
|
|
62
|
+
if (args.policies.length > OZ_LIMITS.maxPoliciesPerRule) {
|
|
63
|
+
throw limitError('INSTALL_BUILD_FAILED', `policies ${args.policies.length} exceed MAX_POLICIES_PER_RULE ${OZ_LIMITS.maxPoliciesPerRule}`);
|
|
64
|
+
}
|
|
65
|
+
if (args.signers.length === 0 && args.policies.length === 0) {
|
|
66
|
+
throw limitError('INSTALL_BUILD_FAILED', 'a rule with no signers and no policies is refused at install (NoSignersAndPolicies)');
|
|
67
|
+
}
|
|
68
|
+
// OZ's spending_limit caps spend on the CONTEXT CONTRACT - it takes no
|
|
69
|
+
// token argument - so it refuses a Default rule on chain with a bare
|
|
70
|
+
// #3227. Say which rule shape it needs here, before the encoding, rather
|
|
71
|
+
// than letting the user read a numeric trap.
|
|
72
|
+
if (args.policies.some((p) => p.kind === 'oz_builtin' && p.primitive.primitive === 'spending_limit')) {
|
|
73
|
+
if (draft.contextRuleType.kind !== 'call_contract') {
|
|
74
|
+
throw limitError('INSTALL_BUILD_FAILED', `spending_limit caps spend on the rule's context contract, so the rule must be call_contract-scoped to that token - a "${draft.contextRuleType.kind}" rule is refused on chain (#3227)`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// ---- 2. context_type encoding ----
|
|
78
|
+
const contextType = encodeContextRuleType(draft.contextRuleType);
|
|
79
|
+
// ---- 3. name (string) + valid_until (Option<u32>) ----
|
|
80
|
+
const name = xdr.ScVal.scvString(draft.name);
|
|
81
|
+
const validUntil = draft.validUntilLedger === null ? xdr.ScVal.scvVoid() : xdr.ScVal.scvU32(draft.validUntilLedger);
|
|
82
|
+
// ---- 4. signers ----
|
|
83
|
+
const signersVec = xdr.ScVal.scvVec(args.signers.map(encodeSigner));
|
|
84
|
+
// ---- 5. policies Map<Address, Val> ----
|
|
85
|
+
const policies = encodePoliciesMap(args);
|
|
86
|
+
return [contextType, name, validUntil, signersVec, policies];
|
|
87
|
+
}
|
|
88
|
+
// ---- helpers ----
|
|
89
|
+
/** Field order of the `PolicyInstallParams` struct. The contract's
|
|
90
|
+
* `#[contracttype]` derives a Map<Symbol, Val> encoding, so the host
|
|
91
|
+
* sorts by symbol STRING (NOT by XDR bytes). This explicit list is the
|
|
92
|
+
* single source of truth for the field order. */
|
|
93
|
+
const POLICY_INSTALL_PARAM_FIELDS = [
|
|
94
|
+
'grammar_version',
|
|
95
|
+
'install_nonce',
|
|
96
|
+
'predicate',
|
|
97
|
+
'predicate_hash',
|
|
98
|
+
'oracle_max_staleness_seconds',
|
|
99
|
+
'oracle_max_deviation_bps',
|
|
100
|
+
// Cross-feed divergence bound. A field added to the contract's
|
|
101
|
+
// `PolicyInstallParams` changes the ABI: the host unpacks the map by field
|
|
102
|
+
// count, so omitting one fails the entire install with
|
|
103
|
+
// `Error(Object, UnexpectedSize)` and no indication of which field is
|
|
104
|
+
// missing. The Rust tests build that struct in Rust and never cross this
|
|
105
|
+
// boundary, so only a real install against a deployed contract catches it.
|
|
106
|
+
'oracle_max_xfeed_dev_bps',
|
|
107
|
+
];
|
|
108
|
+
function encodeContextRuleType(rule) {
|
|
109
|
+
switch (rule.kind) {
|
|
110
|
+
case 'default':
|
|
111
|
+
return xdr.ScVal.scvVec([xdr.ScVal.scvSymbol('Default')]);
|
|
112
|
+
case 'call_contract':
|
|
113
|
+
return xdr.ScVal.scvVec([
|
|
114
|
+
xdr.ScVal.scvSymbol('CallContract'),
|
|
115
|
+
Address.fromString(rule.contract).toScVal(),
|
|
116
|
+
]);
|
|
117
|
+
case 'create_contract':
|
|
118
|
+
return xdr.ScVal.scvVec([
|
|
119
|
+
xdr.ScVal.scvSymbol('CreateContract'),
|
|
120
|
+
xdr.ScVal.scvBytes(Buffer.from(rule.wasmHash, 'hex')),
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function encodeSigner(s) {
|
|
125
|
+
switch (s.kind) {
|
|
126
|
+
case 'delegated':
|
|
127
|
+
return xdr.ScVal.scvVec([
|
|
128
|
+
xdr.ScVal.scvSymbol('Delegated'),
|
|
129
|
+
Address.fromString(s.address).toScVal(),
|
|
130
|
+
]);
|
|
131
|
+
case 'external':
|
|
132
|
+
return xdr.ScVal.scvVec([
|
|
133
|
+
xdr.ScVal.scvSymbol('External'),
|
|
134
|
+
Address.fromString(s.verifier).toScVal(),
|
|
135
|
+
xdr.ScVal.scvBytes(Buffer.from(s.keyBytes, 'hex')),
|
|
136
|
+
]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function encodePoliciesMap(args) {
|
|
140
|
+
const entries = [];
|
|
141
|
+
for (const ref of args.policies) {
|
|
142
|
+
if (ref.kind === 'interpreter') {
|
|
143
|
+
const val = encodePolicyInstallParams(args);
|
|
144
|
+
entries.push(new xdr.ScMapEntry({
|
|
145
|
+
key: Address.fromString(ref.interpreterAddress).toScVal(),
|
|
146
|
+
val,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
if (ref.kind === 'oz_builtin') {
|
|
150
|
+
entries.push(new xdr.ScMapEntry({
|
|
151
|
+
key: Address.fromString(ref.instanceAddress).toScVal(),
|
|
152
|
+
val: encodeOzPrimitiveParams(ref.primitive),
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
entries.sort(sortByScValSymbolString);
|
|
157
|
+
return xdr.ScVal.scvMap(entries);
|
|
158
|
+
}
|
|
159
|
+
/** Encode the install params for one OZ built-in policy.
|
|
160
|
+
*
|
|
161
|
+
* Field names and types are read from the deployed contracts' own spec:
|
|
162
|
+
* SpendingLimitAccountParams { period_ledgers: u32, spending_limit: i128 }
|
|
163
|
+
* SimpleThresholdAccountParams { threshold: u32 }
|
|
164
|
+
* WeightedThresholdAccountParams { signer_weights: map, threshold: u32 }
|
|
165
|
+
* Entries are symbol-string ordered for the same reason the interpreter's
|
|
166
|
+
* are: the host orders by symbol, not by XDR bytes.
|
|
167
|
+
*/
|
|
168
|
+
function encodeOzPrimitiveParams(primitive) {
|
|
169
|
+
const entries = [];
|
|
170
|
+
const push = (name, val) => entries.push(new xdr.ScMapEntry({ key: xdr.ScVal.scvSymbol(name), val }));
|
|
171
|
+
const p = primitive.params;
|
|
172
|
+
switch (primitive.primitive) {
|
|
173
|
+
case 'spending_limit': {
|
|
174
|
+
const limit = p.spending_limit;
|
|
175
|
+
const period = p.period_ledgers;
|
|
176
|
+
if (typeof limit !== 'string' || typeof period !== 'number') {
|
|
177
|
+
throw limitError('INSTALL_BUILD_FAILED', 'spending_limit needs { spending_limit: string; period_ledgers: number }');
|
|
178
|
+
}
|
|
179
|
+
push('period_ledgers', xdr.ScVal.scvU32(period));
|
|
180
|
+
push('spending_limit', encodeI128(limit));
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case 'simple_threshold': {
|
|
184
|
+
const threshold = p.threshold;
|
|
185
|
+
if (typeof threshold !== 'number') {
|
|
186
|
+
throw limitError('INSTALL_BUILD_FAILED', 'simple_threshold needs { threshold: number }');
|
|
187
|
+
}
|
|
188
|
+
push('threshold', xdr.ScVal.scvU32(threshold));
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'weighted_threshold': {
|
|
192
|
+
const threshold = p.threshold;
|
|
193
|
+
const weights = p.weights;
|
|
194
|
+
if (typeof threshold !== 'number' || !weights) {
|
|
195
|
+
throw limitError('INSTALL_BUILD_FAILED', 'weighted_threshold needs { threshold: number; weights: Record<address, number> }');
|
|
196
|
+
}
|
|
197
|
+
const weightEntries = Object.entries(weights)
|
|
198
|
+
.map(([addr, w]) => new xdr.ScMapEntry({
|
|
199
|
+
key: Address.fromString(addr).toScVal(),
|
|
200
|
+
val: xdr.ScVal.scvU32(w),
|
|
201
|
+
}))
|
|
202
|
+
.sort((a, b) => (a.key().toXDR('base64') < b.key().toXDR('base64') ? -1 : 1));
|
|
203
|
+
push('signer_weights', xdr.ScVal.scvMap(weightEntries));
|
|
204
|
+
push('threshold', xdr.ScVal.scvU32(threshold));
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
entries.sort((a, b) => sortBySymbolString(a.key(), b.key()));
|
|
209
|
+
return xdr.ScVal.scvMap(entries);
|
|
210
|
+
}
|
|
211
|
+
function encodeI128(value) {
|
|
212
|
+
const v = BigInt(value);
|
|
213
|
+
return xdr.ScVal.scvI128(new xdr.Int128Parts({
|
|
214
|
+
hi: new xdr.Int64(BigInt.asIntN(64, v >> 64n)),
|
|
215
|
+
lo: new xdr.Uint64(BigInt.asUintN(64, v)),
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
function encodePolicyInstallParams(args) {
|
|
219
|
+
const predicate = Buffer.from(args.encodedPredicate, 'base64');
|
|
220
|
+
const computedHash = createHash('sha256').update(predicate).digest('hex');
|
|
221
|
+
if (computedHash !== args.predicateHash) {
|
|
222
|
+
throw limitError('INSTALL_BUILD_FAILED', `predicateHash ${args.predicateHash.slice(0, 16)}... does not match sha256(encodedPredicate) ${computedHash.slice(0, 16)}...`);
|
|
223
|
+
}
|
|
224
|
+
const entries = [];
|
|
225
|
+
for (const k of POLICY_INSTALL_PARAM_FIELDS) {
|
|
226
|
+
const key = xdr.ScVal.scvSymbol(k);
|
|
227
|
+
switch (k) {
|
|
228
|
+
case 'grammar_version':
|
|
229
|
+
entries.push(new xdr.ScMapEntry({
|
|
230
|
+
key,
|
|
231
|
+
val: xdr.ScVal.scvU32(args.grammarVersion ?? DEFAULT_GRAMMAR_VERSION),
|
|
232
|
+
}));
|
|
233
|
+
break;
|
|
234
|
+
case 'install_nonce':
|
|
235
|
+
entries.push(new xdr.ScMapEntry({ key, val: xdr.ScVal.scvU32(args.installNonce) }));
|
|
236
|
+
break;
|
|
237
|
+
case 'predicate':
|
|
238
|
+
entries.push(new xdr.ScMapEntry({ key, val: xdr.ScVal.scvBytes(predicate) }));
|
|
239
|
+
break;
|
|
240
|
+
case 'predicate_hash':
|
|
241
|
+
entries.push(new xdr.ScMapEntry({
|
|
242
|
+
key,
|
|
243
|
+
val: xdr.ScVal.scvBytes(Buffer.from(args.predicateHash, 'hex')),
|
|
244
|
+
}));
|
|
245
|
+
break;
|
|
246
|
+
case 'oracle_max_staleness_seconds':
|
|
247
|
+
entries.push(new xdr.ScMapEntry({
|
|
248
|
+
key,
|
|
249
|
+
val: encodeOptionU32(args.oracleParams?.maxStalenessSeconds),
|
|
250
|
+
}));
|
|
251
|
+
break;
|
|
252
|
+
case 'oracle_max_deviation_bps':
|
|
253
|
+
entries.push(new xdr.ScMapEntry({
|
|
254
|
+
key,
|
|
255
|
+
val: encodeOptionU32(args.oracleParams?.maxDeviationBps),
|
|
256
|
+
}));
|
|
257
|
+
break;
|
|
258
|
+
case 'oracle_max_xfeed_dev_bps':
|
|
259
|
+
entries.push(new xdr.ScMapEntry({
|
|
260
|
+
key,
|
|
261
|
+
val: encodeOptionU32(args.oracleParams?.maxCrossFeedDeviationBps),
|
|
262
|
+
}));
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// The host orders map entries by the SYMBOL STRING, not by XDR bytes. A
|
|
267
|
+
// length prefix in the XDR encoding would otherwise put `amount` before
|
|
268
|
+
// `address` and produce a struct the contract reads differently. Emit the
|
|
269
|
+
// entries in symbol-string order so the wire form matches the host's view.
|
|
270
|
+
entries.sort((a, b) => sortBySymbolString(a.key(), b.key()));
|
|
271
|
+
return xdr.ScVal.scvMap(entries);
|
|
272
|
+
}
|
|
273
|
+
function encodeOptionU32(v) {
|
|
274
|
+
if (v === undefined)
|
|
275
|
+
return xdr.ScVal.scvVoid();
|
|
276
|
+
if (!Number.isFinite(v) || v < 0 || v > 0xffffffff) {
|
|
277
|
+
throw limitError('INSTALL_BUILD_FAILED', `oracle params value ${v} is outside u32 range`);
|
|
278
|
+
}
|
|
279
|
+
return xdr.ScVal.scvU32(v);
|
|
280
|
+
}
|
|
281
|
+
/** Sort host-map keys by their symbol-string form. The host orders map
|
|
282
|
+
* entries by the SYMBOL STRING, not by the key's XDR bytes (a length
|
|
283
|
+
* prefix in the encoding would otherwise put `amount` before `address`).
|
|
284
|
+
* For non-symbol keys we fall back to length-prefixed XDR bytes; the
|
|
285
|
+
* caller's only Address keys today are length-stable so this is moot, but
|
|
286
|
+
* the helper stands ready for the multi-policy variant. */
|
|
287
|
+
function sortByScValSymbolString(a, b) {
|
|
288
|
+
return sortBySymbolString(a.key(), b.key());
|
|
289
|
+
}
|
|
290
|
+
function sortBySymbolString(a, b) {
|
|
291
|
+
const aSym = a.switch().name === 'scvSymbol' ? a.sym().toString() : null;
|
|
292
|
+
const bSym = b.switch().name === 'scvSymbol' ? b.sym().toString() : null;
|
|
293
|
+
if (aSym !== null && bSym !== null) {
|
|
294
|
+
return aSym < bSym ? -1 : aSym > bSym ? 1 : 0;
|
|
295
|
+
}
|
|
296
|
+
return Buffer.compare(a.toXDR(), b.toXDR());
|
|
297
|
+
}
|
|
298
|
+
function limitError(code, message) {
|
|
299
|
+
const err = new Error(message);
|
|
300
|
+
err.code = code;
|
|
301
|
+
err.severity = 'error';
|
|
302
|
+
err.retryable = false;
|
|
303
|
+
throw err;
|
|
304
|
+
}
|
package/dist/ir/types.d.ts
CHANGED
|
@@ -53,6 +53,10 @@ export interface IRCompare {
|
|
|
53
53
|
selector: IRSelector;
|
|
54
54
|
operator: IRCompOp;
|
|
55
55
|
value: string;
|
|
56
|
+
/** Decimal basis of `value`, for selectors whose comparand is not on a basis
|
|
57
|
+
* the contract can infer. Set for `oracle_price`, where prices normalise to
|
|
58
|
+
* 9 dp and an undeclared basis fails OPEN. */
|
|
59
|
+
valueDecimals?: number;
|
|
56
60
|
}
|
|
57
61
|
/** Condition tree. NEAR-V2 guard/constraint are flat And/Or; the IR allows
|
|
58
62
|
* nesting + `not` + `in` + `eq_seq` so the same IR can later lower to the OZ
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { xdr } from '@stellar/stellar-sdk';
|
|
2
|
+
import type { PredicateLeaf, PredicateNode } from '../types.ts';
|
|
3
|
+
/** One leaf. Order of the bare-literal checks mirrors `decode_leaf` in dsl.rs. */
|
|
4
|
+
export declare function decodeLeaf(v: xdr.ScVal): PredicateLeaf;
|
|
5
|
+
/** One node. `not` wraps a NODE; the comparison ops wrap two LEAVES. */
|
|
6
|
+
export declare function decodeNode(v: xdr.ScVal): PredicateNode;
|
|
7
|
+
/** Decode a predicate from the wire bytes the interpreter stores.
|
|
8
|
+
*
|
|
9
|
+
* Accepts the base64 the encoder emits (`encodePredicate().encodedPredicate`)
|
|
10
|
+
* or the raw XDR bytes read straight out of `StoredDoc.predicate_bytes`. */
|
|
11
|
+
export declare function decodePredicate(encoded: string | Uint8Array): PredicateNode;
|