@crediolabs/policy-synth 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/interpreter/adapter.d.ts +2 -2
- package/dist/adapters/interpreter/adapter.js +11 -3
- package/dist/errors.d.ts +6 -1
- package/dist/install/authority-overlap.js +12 -0
- package/dist/install/build-add-context-rule.d.ts +1 -1
- package/dist/install/read-account-rules.d.ts +79 -0
- package/dist/install/read-account-rules.js +241 -0
- package/dist/predicate/decode.js +22 -1
- package/dist/predicate/encode.js +52 -5
- package/dist/predicate/from-json.js +21 -1
- package/dist/review-card/builder.js +40 -0
- package/dist/review-card/cross-check.js +34 -0
- package/dist/review-card/render-leaf.d.ts +1 -1
- package/dist/review-card/render-leaf.js +7 -0
- package/dist/run/index.d.ts +2 -2
- package/dist/run/index.js +52 -9
- package/dist/run/schemas.d.ts +64 -14
- package/dist/run/schemas.js +70 -6
- package/dist/simulate/deny-cases.js +11 -0
- package/dist/simulate/evaluate.js +86 -5
- package/dist/synth/declare.d.ts +13 -0
- package/dist/synth/declare.js +34 -5
- package/dist/synth/synthesize-from-recording.js +1 -1
- package/dist/types.d.ts +21 -1
- package/dist/types.js +1 -1
- package/dist-cjs/adapters/interpreter/adapter.d.ts +2 -2
- package/dist-cjs/adapters/interpreter/adapter.js +11 -3
- package/dist-cjs/errors.d.ts +6 -1
- package/dist-cjs/install/authority-overlap.js +12 -0
- package/dist-cjs/install/build-add-context-rule.d.ts +1 -1
- package/dist-cjs/install/read-account-rules.d.ts +79 -0
- package/dist-cjs/install/read-account-rules.js +252 -0
- package/dist-cjs/predicate/decode.js +22 -1
- package/dist-cjs/predicate/encode.js +52 -5
- package/dist-cjs/predicate/from-json.js +21 -1
- package/dist-cjs/review-card/builder.js +40 -0
- package/dist-cjs/review-card/cross-check.js +34 -0
- package/dist-cjs/review-card/render-leaf.d.ts +1 -1
- package/dist-cjs/review-card/render-leaf.js +7 -0
- package/dist-cjs/run/index.d.ts +2 -2
- package/dist-cjs/run/index.js +54 -9
- package/dist-cjs/run/schemas.d.ts +64 -14
- package/dist-cjs/run/schemas.js +71 -7
- package/dist-cjs/simulate/deny-cases.js +11 -0
- package/dist-cjs/simulate/evaluate.js +86 -5
- package/dist-cjs/synth/declare.d.ts +13 -0
- package/dist-cjs/synth/declare.js +34 -5
- package/dist-cjs/synth/synthesize-from-recording.js +1 -1
- package/dist-cjs/types.d.ts +21 -1
- package/dist-cjs/types.js +1 -1
- package/package.json +1 -1
- package/src/adapters/interpreter/adapter.ts +13 -5
- package/src/errors.ts +5 -0
- package/src/install/authority-overlap.ts +11 -0
- package/src/install/read-account-rules.ts +313 -0
- package/src/predicate/decode.ts +22 -1
- package/src/predicate/encode.ts +55 -5
- package/src/predicate/from-json.ts +21 -1
- package/src/review-card/builder.ts +45 -2
- package/src/review-card/cross-check.ts +35 -1
- package/src/review-card/render-leaf.ts +8 -1
- package/src/run/index.ts +57 -8
- package/src/run/schemas.ts +83 -6
- package/src/simulate/deny-cases.ts +12 -1
- package/src/simulate/evaluate.ts +101 -9
- package/src/synth/declare.ts +54 -5
- package/src/synth/synthesize-from-recording.ts +1 -1
- package/src/types.ts +16 -1
package/dist-cjs/run/index.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// No business logic. No retries. No session state. The same call shape can
|
|
19
19
|
// drive the CLI (which calls into the same core directly without MCP).
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.ToolErrorSchema = exports.TESTNET_RPC_URL = exports.SynthesizePolicyInputSchema = exports.RPC_URL_BY_NETWORK = exports.RevokePolicyInputSchema = exports.RecordTransactionInputSchema = exports.RecordedTransactionSchema = exports.PredicateNodeSchema = exports.PredicateLeafSchema = exports.PINNED_INTERPRETER_WASM_SHA256 = exports.PINNED_INTERPRETER_TESTNET_ADDRESS = exports.PINNED_INTERPRETER_MAINNET_ADDRESS = exports.PINNED_INTERPRETER_GRAMMAR_VERSION = exports.PINNED_INTERPRETER_ADDRESS_BY_NETWORK = exports.NetworkSchema = exports.MAINNET_RPC_URL = exports.InterpreterOptionsSchema = exports.InstallPolicyInputSchema = exports.GetInterpreterInfoInputSchema = exports.DeclarePolicyInputSchema = exports.ComposeUserResponsesSchema = void 0;
|
|
21
|
+
exports.ToolErrorSchema = exports.TESTNET_RPC_URL = exports.SynthesizePolicyInputSchema = exports.RPC_URL_BY_NETWORK = exports.RevokePolicyInputSchema = exports.RecordTransactionInputSchema = exports.RecordedTransactionSchema = exports.PredicateNodeSchema = exports.PredicateLeafSchema = exports.PINNED_OZ_POLICY_WASM_SHA256 = exports.PINNED_OZ_POLICY_ADDRESS_BY_NETWORK = exports.PINNED_INTERPRETER_WASM_SHA256 = exports.PINNED_INTERPRETER_TESTNET_ADDRESS = exports.PINNED_INTERPRETER_MAINNET_ADDRESS = exports.PINNED_INTERPRETER_GRAMMAR_VERSION = exports.PINNED_INTERPRETER_ADDRESS_BY_NETWORK = exports.NetworkSchema = exports.MAINNET_RPC_URL = exports.InterpreterOptionsSchema = exports.InstallPolicyInputSchema = exports.GetInterpreterInfoInputSchema = exports.DeclarePolicyInputSchema = exports.ComposeUserResponsesSchema = void 0;
|
|
22
22
|
exports.runRecordTransaction = runRecordTransaction;
|
|
23
23
|
exports.runSynthesizePolicy = runSynthesizePolicy;
|
|
24
24
|
exports.runInstallPolicy = runInstallPolicy;
|
|
@@ -34,6 +34,7 @@ const index_ts_1 = require("../index.js");
|
|
|
34
34
|
const authority_overlap_ts_1 = require("../install/authority-overlap.js");
|
|
35
35
|
const build_install_policy_ts_1 = require("../install/build-install-policy.js");
|
|
36
36
|
const get_interpreter_info_ts_1 = require("../install/get-interpreter-info.js");
|
|
37
|
+
const read_account_rules_ts_1 = require("../install/read-account-rules.js");
|
|
37
38
|
const decode_ts_1 = require("../predicate/decode.js");
|
|
38
39
|
const index_ts_2 = require("../simulate/index.js");
|
|
39
40
|
const schemas_ts_1 = require("./schemas.js");
|
|
@@ -54,6 +55,8 @@ Object.defineProperty(exports, "PINNED_INTERPRETER_GRAMMAR_VERSION", { enumerabl
|
|
|
54
55
|
Object.defineProperty(exports, "PINNED_INTERPRETER_MAINNET_ADDRESS", { enumerable: true, get: function () { return schemas_ts_2.PINNED_INTERPRETER_MAINNET_ADDRESS; } });
|
|
55
56
|
Object.defineProperty(exports, "PINNED_INTERPRETER_TESTNET_ADDRESS", { enumerable: true, get: function () { return schemas_ts_2.PINNED_INTERPRETER_TESTNET_ADDRESS; } });
|
|
56
57
|
Object.defineProperty(exports, "PINNED_INTERPRETER_WASM_SHA256", { enumerable: true, get: function () { return schemas_ts_2.PINNED_INTERPRETER_WASM_SHA256; } });
|
|
58
|
+
Object.defineProperty(exports, "PINNED_OZ_POLICY_ADDRESS_BY_NETWORK", { enumerable: true, get: function () { return schemas_ts_2.PINNED_OZ_POLICY_ADDRESS_BY_NETWORK; } });
|
|
59
|
+
Object.defineProperty(exports, "PINNED_OZ_POLICY_WASM_SHA256", { enumerable: true, get: function () { return schemas_ts_2.PINNED_OZ_POLICY_WASM_SHA256; } });
|
|
57
60
|
Object.defineProperty(exports, "PredicateLeafSchema", { enumerable: true, get: function () { return schemas_ts_2.PredicateLeafSchema; } });
|
|
58
61
|
Object.defineProperty(exports, "PredicateNodeSchema", { enumerable: true, get: function () { return schemas_ts_2.PredicateNodeSchema; } });
|
|
59
62
|
Object.defineProperty(exports, "RecordedTransactionSchema", { enumerable: true, get: function () { return schemas_ts_2.RecordedTransactionSchema; } });
|
|
@@ -184,11 +187,18 @@ async function runInstallPolicy(raw) {
|
|
|
184
187
|
rpc: rpcClient,
|
|
185
188
|
...(input.baseFee !== undefined ? { baseFee: input.baseFee } : {}),
|
|
186
189
|
});
|
|
187
|
-
// Cross-rule scan
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
190
|
+
// Cross-rule scan. The caller may supply `existingRules` (useful offline,
|
|
191
|
+
// and for testing); otherwise the account is READ, so the answer describes
|
|
192
|
+
// what is actually installed rather than what the caller happened to
|
|
193
|
+
// mention.
|
|
194
|
+
//
|
|
195
|
+
// `null` means NOT CHECKED and is returned whenever the scan cannot be
|
|
196
|
+
// trusted to be complete - the read failed, or it stopped before
|
|
197
|
+
// accounting for every live rule. An empty list would say "checked,
|
|
198
|
+
// nothing found", and a partial scan that reported `[]` would be claiming
|
|
199
|
+
// a safety it never established.
|
|
200
|
+
const observed = await resolveExistingRules(input, network, expectedInterpreter);
|
|
201
|
+
const authorityScan = observed === null
|
|
192
202
|
? null
|
|
193
203
|
: (0, authority_overlap_ts_1.findAuthorityOverlaps)({
|
|
194
204
|
intended: {
|
|
@@ -200,9 +210,7 @@ async function runInstallPolicy(raw) {
|
|
|
200
210
|
signers: input.rule.signers,
|
|
201
211
|
predicate: (0, decode_ts_1.decodePredicate)(encodedPredicate),
|
|
202
212
|
},
|
|
203
|
-
|
|
204
|
-
// PredicateNodeSchema); the shape is already validated.
|
|
205
|
-
existing: input.existingRules,
|
|
213
|
+
existing: observed,
|
|
206
214
|
});
|
|
207
215
|
return { ok: true, data: { ...result, authorityScan } };
|
|
208
216
|
}
|
|
@@ -353,6 +361,7 @@ function runDeclarePolicy(raw) {
|
|
|
353
361
|
...(d.recipients !== undefined ? { recipients: d.recipients } : {}),
|
|
354
362
|
...(d.recipientArgIndex !== undefined ? { recipientArgIndex: d.recipientArgIndex } : {}),
|
|
355
363
|
...(d.allowZeroCap !== undefined ? { allowZeroCap: d.allowZeroCap } : {}),
|
|
364
|
+
...(d.minOutputRatio !== undefined ? { minOutputRatio: d.minOutputRatio } : {}),
|
|
356
365
|
});
|
|
357
366
|
const { encodedPredicate, predicateHash } = (0, index_ts_1.encodePredicate)(predicate);
|
|
358
367
|
return { ok: true, data: { predicate, encodedPredicate, predicateHash, warnings } };
|
|
@@ -448,6 +457,42 @@ async function runGetInterpreterInfo(raw) {
|
|
|
448
457
|
* network, falling back to the pinned RPC for the network. The caller
|
|
449
458
|
* has already been gated against the pinned URL elsewhere, so the
|
|
450
459
|
* fallback here only ever picks from a finite, audited pair. */
|
|
460
|
+
/** The account's other context rules, or `null` when they could not be
|
|
461
|
+
* established completely.
|
|
462
|
+
*
|
|
463
|
+
* Caller-supplied `existingRules` win: they let the scan run offline, and a
|
|
464
|
+
* caller who passes them has said what to compare against. Otherwise the
|
|
465
|
+
* account is read over RPC.
|
|
466
|
+
*
|
|
467
|
+
* Every failure path returns `null` rather than a short list. A read that
|
|
468
|
+
* threw, or one that stopped before accounting for every live rule, has not
|
|
469
|
+
* ruled anything out - and reporting `[]` there would turn "we could not
|
|
470
|
+
* check" into "there is nothing to worry about". */
|
|
471
|
+
async function resolveExistingRules(input, network, interpreterAddress) {
|
|
472
|
+
if (input.existingRules !== undefined) {
|
|
473
|
+
// The schema types `predicate` loosely (it is the shared
|
|
474
|
+
// PredicateNodeSchema); the shape is already validated.
|
|
475
|
+
return input.existingRules;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const url = input.rpcUrl ?? schemas_ts_1.RPC_URL_BY_NETWORK[network];
|
|
479
|
+
const server = new stellar_sdk_1.rpc.Server(url, { allowHttp: false });
|
|
480
|
+
const collected = await (0, read_account_rules_ts_1.collectObservedRules)({
|
|
481
|
+
reader: (0, read_account_rules_ts_1.accountRuleReaderFromServer)(server, schemas_ts_1.NETWORK_PASSPHRASES[network]),
|
|
482
|
+
smartAccount: input.smartAccount,
|
|
483
|
+
interpreterAddress,
|
|
484
|
+
});
|
|
485
|
+
if (collected.incomplete)
|
|
486
|
+
return null;
|
|
487
|
+
return collected.rules;
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
// The install itself is unaffected: the scan is advisory, so a failed
|
|
491
|
+
// read must not block a policy the user asked for. It just cannot be
|
|
492
|
+
// reported as a clean scan.
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
451
496
|
function buildRpcClientFromInput(urlOverride, network) {
|
|
452
497
|
const url = urlOverride ?? schemas_ts_1.RPC_URL_BY_NETWORK[network];
|
|
453
498
|
const passphrase = schemas_ts_1.NETWORK_PASSPHRASES[network];
|
|
@@ -1559,6 +1559,7 @@ export declare const ObservedRuleSchema: z.ZodObject<{
|
|
|
1559
1559
|
keyBytes: string;
|
|
1560
1560
|
})[];
|
|
1561
1561
|
id: number;
|
|
1562
|
+
policyAddresses: string[];
|
|
1562
1563
|
contextType: {
|
|
1563
1564
|
kind: "default";
|
|
1564
1565
|
} | {
|
|
@@ -1568,7 +1569,6 @@ export declare const ObservedRuleSchema: z.ZodObject<{
|
|
|
1568
1569
|
kind: "create_contract";
|
|
1569
1570
|
wasmHash: string;
|
|
1570
1571
|
};
|
|
1571
|
-
policyAddresses: string[];
|
|
1572
1572
|
predicate?: unknown;
|
|
1573
1573
|
}, {
|
|
1574
1574
|
signers: ({
|
|
@@ -1580,6 +1580,7 @@ export declare const ObservedRuleSchema: z.ZodObject<{
|
|
|
1580
1580
|
keyBytes: string;
|
|
1581
1581
|
})[];
|
|
1582
1582
|
id: number;
|
|
1583
|
+
policyAddresses: string[];
|
|
1583
1584
|
contextType: {
|
|
1584
1585
|
kind: "default";
|
|
1585
1586
|
} | {
|
|
@@ -1589,28 +1590,27 @@ export declare const ObservedRuleSchema: z.ZodObject<{
|
|
|
1589
1590
|
kind: "create_contract";
|
|
1590
1591
|
wasmHash: string;
|
|
1591
1592
|
};
|
|
1592
|
-
policyAddresses: string[];
|
|
1593
1593
|
predicate?: unknown;
|
|
1594
1594
|
}>;
|
|
1595
1595
|
/** Pinned interpreter address (testnet).
|
|
1596
1596
|
* Single source for the MCP layer; do not embed elsewhere. */
|
|
1597
|
-
export declare const PINNED_INTERPRETER_TESTNET_ADDRESS = "
|
|
1598
|
-
/** Pinned interpreter address (mainnet), redeployed 2026-08-22 from a reproducible build. The mainnet
|
|
1597
|
+
export declare const PINNED_INTERPRETER_TESTNET_ADDRESS = "CCBHVZ6HGGV7C4SNHCZ3S5665Z2WEMHTMBAEPO4XW6PKON464BEBANU5";
|
|
1598
|
+
/** Pinned interpreter address (mainnet), redeployed 2026-08-22 for grammar 4, from a reproducible build. The mainnet
|
|
1599
1599
|
* interpreter IS the binary exercised on testnet - both instances were created
|
|
1600
1600
|
* from the same uploaded wasm hash (see PINNED_INTERPRETER_WASM_SHA256), and
|
|
1601
|
-
* both were read back with `grammar_version()` returning
|
|
1601
|
+
* both were read back with `grammar_version()` returning 4. The address differs
|
|
1602
1602
|
* because instance ids are network-scoped. UNAUDITED at the time of writing.
|
|
1603
1603
|
*
|
|
1604
1604
|
* These four constants move together or not at all. The grammar version and
|
|
1605
1605
|
* wasm hash are single values covering BOTH networks, so re-pinning one network
|
|
1606
1606
|
* alone would have the builder emit a version the other network refuses - with
|
|
1607
1607
|
* a green test run, since `grammar-version-parity.test.ts` would then pass. */
|
|
1608
|
-
export declare const PINNED_INTERPRETER_MAINNET_ADDRESS = "
|
|
1608
|
+
export declare const PINNED_INTERPRETER_MAINNET_ADDRESS = "CDN755TDYZM3ZQ5OXTJ6TIBUBWZV2KRI2BYJPBXD2MVWED4STT3VBN52";
|
|
1609
1609
|
/** Pinned interpreter wasm sha256 (hex). */
|
|
1610
|
-
export declare const PINNED_INTERPRETER_WASM_SHA256 = "
|
|
1610
|
+
export declare const PINNED_INTERPRETER_WASM_SHA256 = "b5ba1e35ccf20cd8c13c3a2c3098bf337033a92bcaf475d63c03ddc0cba0fcae";
|
|
1611
1611
|
/** The grammar version the interpreter enforces (matches SELF_VERSION in
|
|
1612
1612
|
* contracts/policy-interpreter/src/version.rs). */
|
|
1613
|
-
export declare const PINNED_INTERPRETER_GRAMMAR_VERSION =
|
|
1613
|
+
export declare const PINNED_INTERPRETER_GRAMMAR_VERSION = 4;
|
|
1614
1614
|
/** Default Soroban RPC for the install / revoke / info tools. The recorder
|
|
1615
1615
|
* keeps its own copy in record/rpc.ts because it hands back a fetcher rather
|
|
1616
1616
|
* than a Server; the two are deliberately different surfaces, so this is
|
|
@@ -1627,6 +1627,25 @@ export declare const MAINNET_RPC_URL = "https://mainnet.sorobanrpc.com";
|
|
|
1627
1627
|
* a single constant - only the addresses and RPCs are network-scoped. */
|
|
1628
1628
|
export declare const PINNED_INTERPRETER_ADDRESS_BY_NETWORK: Record<Network, string>;
|
|
1629
1629
|
export declare const RPC_URL_BY_NETWORK: Record<Network, string>;
|
|
1630
|
+
/** The OpenZeppelin built-in policies we deployed instances of. These are OZ
|
|
1631
|
+
* EXAMPLE contracts, built by us from `OpenZeppelin/stellar-contracts` at tag
|
|
1632
|
+
* v0.7.2 and deployed by us. We have NOT audited them and upstream ships an
|
|
1633
|
+
* "experimental software ... as is" disclaimer; anything surfacing one of
|
|
1634
|
+
* these to a user must say so rather than implying we vouch for the code.
|
|
1635
|
+
* Provenance detail in `docs/audit/README.md` finding 7. */
|
|
1636
|
+
export type OzBuiltinPolicy = 'spending_limit' | 'simple_threshold' | 'weighted_threshold';
|
|
1637
|
+
/** Instance addresses per network. Exported so consumers import the pin
|
|
1638
|
+
* instead of copying a literal - a copied address is how a testnet id ends up
|
|
1639
|
+
* being queried against mainnet, which returns `Error(Storage, MissingValue)`
|
|
1640
|
+
* and reads exactly like "nothing is deployed there".
|
|
1641
|
+
*
|
|
1642
|
+
* Instance ids are network-scoped, so the addresses differ while the wasm
|
|
1643
|
+
* hash does not: each pair below was created from the same uploaded wasm (see
|
|
1644
|
+
* `PINNED_OZ_POLICY_WASM_SHA256`), verified by fetching the deployed bytes
|
|
1645
|
+
* back from both networks. */
|
|
1646
|
+
export declare const PINNED_OZ_POLICY_ADDRESS_BY_NETWORK: Record<Network, Record<OzBuiltinPolicy, string>>;
|
|
1647
|
+
/** sha256 of each deployed policy wasm, identical across both networks. */
|
|
1648
|
+
export declare const PINNED_OZ_POLICY_WASM_SHA256: Record<OzBuiltinPolicy, string>;
|
|
1630
1649
|
/** Stellar network passphrases. Pinned here so the XDR envelope uses the
|
|
1631
1650
|
* matching passphrase when the wallet signs (a mismatch yields invalid
|
|
1632
1651
|
* hashes). */
|
|
@@ -1641,6 +1660,25 @@ export declare const DeclarePolicyInputSchema: z.ZodObject<{
|
|
|
1641
1660
|
recipients: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
1642
1661
|
recipientArgIndex: z.ZodOptional<z.ZodNumber>;
|
|
1643
1662
|
allowZeroCap: z.ZodOptional<z.ZodBoolean>;
|
|
1663
|
+
/** Minimum output as a ratio of the call's own input. num/den are decimal
|
|
1664
|
+
* STRINGS for the same reason maxAmount is: an i128 ratio does not
|
|
1665
|
+
* survive a JS number. */
|
|
1666
|
+
minOutputRatio: z.ZodOptional<z.ZodObject<{
|
|
1667
|
+
num: z.ZodString;
|
|
1668
|
+
den: z.ZodString;
|
|
1669
|
+
inputArgIndex: z.ZodNumber;
|
|
1670
|
+
outputArgIndex: z.ZodNumber;
|
|
1671
|
+
}, "strict", z.ZodTypeAny, {
|
|
1672
|
+
num: string;
|
|
1673
|
+
den: string;
|
|
1674
|
+
inputArgIndex: number;
|
|
1675
|
+
outputArgIndex: number;
|
|
1676
|
+
}, {
|
|
1677
|
+
num: string;
|
|
1678
|
+
den: string;
|
|
1679
|
+
inputArgIndex: number;
|
|
1680
|
+
outputArgIndex: number;
|
|
1681
|
+
}>>;
|
|
1644
1682
|
}, "strict", z.ZodTypeAny, {
|
|
1645
1683
|
fn: string;
|
|
1646
1684
|
contract?: string | undefined;
|
|
@@ -1649,6 +1687,12 @@ export declare const DeclarePolicyInputSchema: z.ZodObject<{
|
|
|
1649
1687
|
recipients?: string[] | undefined;
|
|
1650
1688
|
recipientArgIndex?: number | undefined;
|
|
1651
1689
|
allowZeroCap?: boolean | undefined;
|
|
1690
|
+
minOutputRatio?: {
|
|
1691
|
+
num: string;
|
|
1692
|
+
den: string;
|
|
1693
|
+
inputArgIndex: number;
|
|
1694
|
+
outputArgIndex: number;
|
|
1695
|
+
} | undefined;
|
|
1652
1696
|
}, {
|
|
1653
1697
|
fn: string;
|
|
1654
1698
|
contract?: string | undefined;
|
|
@@ -1657,6 +1701,12 @@ export declare const DeclarePolicyInputSchema: z.ZodObject<{
|
|
|
1657
1701
|
recipients?: string[] | undefined;
|
|
1658
1702
|
recipientArgIndex?: number | undefined;
|
|
1659
1703
|
allowZeroCap?: boolean | undefined;
|
|
1704
|
+
minOutputRatio?: {
|
|
1705
|
+
num: string;
|
|
1706
|
+
den: string;
|
|
1707
|
+
inputArgIndex: number;
|
|
1708
|
+
outputArgIndex: number;
|
|
1709
|
+
} | undefined;
|
|
1660
1710
|
}>;
|
|
1661
1711
|
export type DeclarePolicyInput = z.infer<typeof DeclarePolicyInputSchema>;
|
|
1662
1712
|
export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
@@ -1726,6 +1776,7 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1726
1776
|
keyBytes: string;
|
|
1727
1777
|
})[];
|
|
1728
1778
|
id: number;
|
|
1779
|
+
policyAddresses: string[];
|
|
1729
1780
|
contextType: {
|
|
1730
1781
|
kind: "default";
|
|
1731
1782
|
} | {
|
|
@@ -1735,7 +1786,6 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1735
1786
|
kind: "create_contract";
|
|
1736
1787
|
wasmHash: string;
|
|
1737
1788
|
};
|
|
1738
|
-
policyAddresses: string[];
|
|
1739
1789
|
predicate?: unknown;
|
|
1740
1790
|
}, {
|
|
1741
1791
|
signers: ({
|
|
@@ -1747,6 +1797,7 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1747
1797
|
keyBytes: string;
|
|
1748
1798
|
})[];
|
|
1749
1799
|
id: number;
|
|
1800
|
+
policyAddresses: string[];
|
|
1750
1801
|
contextType: {
|
|
1751
1802
|
kind: "default";
|
|
1752
1803
|
} | {
|
|
@@ -1756,7 +1807,6 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
1756
1807
|
kind: "create_contract";
|
|
1757
1808
|
wasmHash: string;
|
|
1758
1809
|
};
|
|
1759
|
-
policyAddresses: string[];
|
|
1760
1810
|
predicate?: unknown;
|
|
1761
1811
|
}>, "many">>;
|
|
1762
1812
|
/** The smart account contract address (C...) that will receive the rule. */
|
|
@@ -2153,6 +2203,7 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
2153
2203
|
keyBytes: string;
|
|
2154
2204
|
})[];
|
|
2155
2205
|
id: number;
|
|
2206
|
+
policyAddresses: string[];
|
|
2156
2207
|
contextType: {
|
|
2157
2208
|
kind: "default";
|
|
2158
2209
|
} | {
|
|
@@ -2162,7 +2213,6 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
2162
2213
|
kind: "create_contract";
|
|
2163
2214
|
wasmHash: string;
|
|
2164
2215
|
};
|
|
2165
|
-
policyAddresses: string[];
|
|
2166
2216
|
predicate?: unknown;
|
|
2167
2217
|
}[] | undefined;
|
|
2168
2218
|
rpcUrl?: string | undefined;
|
|
@@ -2212,6 +2262,7 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
2212
2262
|
keyBytes: string;
|
|
2213
2263
|
})[];
|
|
2214
2264
|
id: number;
|
|
2265
|
+
policyAddresses: string[];
|
|
2215
2266
|
contextType: {
|
|
2216
2267
|
kind: "default";
|
|
2217
2268
|
} | {
|
|
@@ -2221,7 +2272,6 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
2221
2272
|
kind: "create_contract";
|
|
2222
2273
|
wasmHash: string;
|
|
2223
2274
|
};
|
|
2224
|
-
policyAddresses: string[];
|
|
2225
2275
|
predicate?: unknown;
|
|
2226
2276
|
}[] | undefined;
|
|
2227
2277
|
rpcUrl?: string | undefined;
|
|
@@ -2271,6 +2321,7 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
2271
2321
|
keyBytes: string;
|
|
2272
2322
|
})[];
|
|
2273
2323
|
id: number;
|
|
2324
|
+
policyAddresses: string[];
|
|
2274
2325
|
contextType: {
|
|
2275
2326
|
kind: "default";
|
|
2276
2327
|
} | {
|
|
@@ -2280,7 +2331,6 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
2280
2331
|
kind: "create_contract";
|
|
2281
2332
|
wasmHash: string;
|
|
2282
2333
|
};
|
|
2283
|
-
policyAddresses: string[];
|
|
2284
2334
|
predicate?: unknown;
|
|
2285
2335
|
}[] | undefined;
|
|
2286
2336
|
rpcUrl?: string | undefined;
|
|
@@ -2330,6 +2380,7 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
2330
2380
|
keyBytes: string;
|
|
2331
2381
|
})[];
|
|
2332
2382
|
id: number;
|
|
2383
|
+
policyAddresses: string[];
|
|
2333
2384
|
contextType: {
|
|
2334
2385
|
kind: "default";
|
|
2335
2386
|
} | {
|
|
@@ -2339,7 +2390,6 @@ export declare const InstallPolicyInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
2339
2390
|
kind: "create_contract";
|
|
2340
2391
|
wasmHash: string;
|
|
2341
2392
|
};
|
|
2342
|
-
policyAddresses: string[];
|
|
2343
2393
|
predicate?: unknown;
|
|
2344
2394
|
}[] | undefined;
|
|
2345
2395
|
rpcUrl?: string | undefined;
|
package/dist-cjs/run/schemas.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// imports them here so its tool-shape bindings stay in step; the CLI imports
|
|
17
17
|
// them here so it can build the same args envelope the MCP transport builds.
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.ToolErrorSchema = exports.GetInterpreterInfoInputSchema = exports.RevokePolicyInputSchema = exports.InstallPolicyInputSchema = exports.DeclarePolicyInputSchema = exports.NETWORK_PASSPHRASES = exports.RPC_URL_BY_NETWORK = exports.PINNED_INTERPRETER_ADDRESS_BY_NETWORK = exports.MAINNET_RPC_URL = exports.TESTNET_RPC_URL = exports.PINNED_INTERPRETER_GRAMMAR_VERSION = exports.PINNED_INTERPRETER_WASM_SHA256 = exports.PINNED_INTERPRETER_MAINNET_ADDRESS = exports.PINNED_INTERPRETER_TESTNET_ADDRESS = exports.ObservedRuleSchema = exports.VerifyPolicyInputSchema = exports.SimulatePolicyInputSchema = exports.PredicateNodeSchema = exports.PredicateLeafSchema = exports.SynthesizePolicyInputSchema = exports.InterpreterOptionsSchema = exports.RecordTransactionInputSchema = exports.ComposeUserResponsesSchema = exports.RecordedTransactionSchema = exports.ParseConfidenceSchema = exports.OnChainEventSchema = exports.TokenMovementSchema = exports.ContractInvocationSchema = exports.ScValSchema = exports.NetworkSchema = void 0;
|
|
19
|
+
exports.ToolErrorSchema = exports.GetInterpreterInfoInputSchema = exports.RevokePolicyInputSchema = exports.InstallPolicyInputSchema = exports.DeclarePolicyInputSchema = exports.NETWORK_PASSPHRASES = exports.PINNED_OZ_POLICY_WASM_SHA256 = exports.PINNED_OZ_POLICY_ADDRESS_BY_NETWORK = exports.RPC_URL_BY_NETWORK = exports.PINNED_INTERPRETER_ADDRESS_BY_NETWORK = exports.MAINNET_RPC_URL = exports.TESTNET_RPC_URL = exports.PINNED_INTERPRETER_GRAMMAR_VERSION = exports.PINNED_INTERPRETER_WASM_SHA256 = exports.PINNED_INTERPRETER_MAINNET_ADDRESS = exports.PINNED_INTERPRETER_TESTNET_ADDRESS = exports.ObservedRuleSchema = exports.VerifyPolicyInputSchema = exports.SimulatePolicyInputSchema = exports.PredicateNodeSchema = exports.PredicateLeafSchema = exports.SynthesizePolicyInputSchema = exports.InterpreterOptionsSchema = exports.RecordTransactionInputSchema = exports.ComposeUserResponsesSchema = exports.RecordedTransactionSchema = exports.ParseConfidenceSchema = exports.OnChainEventSchema = exports.TokenMovementSchema = exports.ContractInvocationSchema = exports.ScValSchema = exports.NetworkSchema = void 0;
|
|
20
20
|
const zod_1 = require("zod");
|
|
21
21
|
const address_ts_1 = require("../synth/address.js");
|
|
22
22
|
/** Soroban `valid_until` is a u32 ledger sequence; a value above this cannot be
|
|
@@ -180,6 +180,15 @@ exports.PredicateLeafSchema = zod_1.z.lazy(() => zod_1.z.union([
|
|
|
180
180
|
element: zod_1.z.number().int().nonnegative(),
|
|
181
181
|
field: zod_1.z.string(),
|
|
182
182
|
}),
|
|
183
|
+
// num/den are i128 decimal strings, matching `literal_i128`. The regex
|
|
184
|
+
// is the boundary guard; the ratio's SIGN is checked at encode, where
|
|
185
|
+
// the message can explain that a negative ratio inverts the comparison.
|
|
186
|
+
zod_1.z.object({
|
|
187
|
+
kind: zod_1.z.literal('call_arg_scaled'),
|
|
188
|
+
index: zod_1.z.number().int().nonnegative(),
|
|
189
|
+
num: zod_1.z.string().regex(/^-?[0-9]+$/),
|
|
190
|
+
den: zod_1.z.string().regex(/^-?[0-9]+$/),
|
|
191
|
+
}),
|
|
183
192
|
zod_1.z.object({ kind: zod_1.z.literal('literal_address'), value: zod_1.z.string() }),
|
|
184
193
|
zod_1.z.object({ kind: zod_1.z.literal('literal_i128'), value: zod_1.z.string().regex(/^-?[0-9]+$/) }),
|
|
185
194
|
zod_1.z.object({ kind: zod_1.z.literal('literal_symbol'), value: zod_1.z.string() }),
|
|
@@ -198,16 +207,32 @@ exports.PredicateLeafSchema = zod_1.z.lazy(() => zod_1.z.union([
|
|
|
198
207
|
* `and`; the lazy + annotation pattern keeps the recursion type-safe. */
|
|
199
208
|
exports.PredicateNodeSchema = zod_1.z.lazy(() => zod_1.z.union([
|
|
200
209
|
zod_1.z.object({ op: zod_1.z.literal('and'), children: zod_1.z.array(exports.PredicateNodeSchema) }),
|
|
210
|
+
zod_1.z.object({ op: zod_1.z.literal('or'), children: zod_1.z.array(exports.PredicateNodeSchema) }),
|
|
201
211
|
zod_1.z.object({
|
|
202
212
|
op: zod_1.z.literal('eq'),
|
|
203
213
|
left: exports.PredicateLeafSchema,
|
|
204
214
|
right: exports.PredicateLeafSchema,
|
|
205
215
|
}),
|
|
216
|
+
zod_1.z.object({
|
|
217
|
+
op: zod_1.z.literal('lt'),
|
|
218
|
+
left: exports.PredicateLeafSchema,
|
|
219
|
+
right: exports.PredicateLeafSchema,
|
|
220
|
+
}),
|
|
206
221
|
zod_1.z.object({
|
|
207
222
|
op: zod_1.z.literal('lte'),
|
|
208
223
|
left: exports.PredicateLeafSchema,
|
|
209
224
|
right: exports.PredicateLeafSchema,
|
|
210
225
|
}),
|
|
226
|
+
zod_1.z.object({
|
|
227
|
+
op: zod_1.z.literal('gt'),
|
|
228
|
+
left: exports.PredicateLeafSchema,
|
|
229
|
+
right: exports.PredicateLeafSchema,
|
|
230
|
+
}),
|
|
231
|
+
zod_1.z.object({
|
|
232
|
+
op: zod_1.z.literal('gte'),
|
|
233
|
+
left: exports.PredicateLeafSchema,
|
|
234
|
+
right: exports.PredicateLeafSchema,
|
|
235
|
+
}),
|
|
211
236
|
zod_1.z.object({
|
|
212
237
|
op: zod_1.z.literal('in'),
|
|
213
238
|
needle: exports.PredicateLeafSchema,
|
|
@@ -303,23 +328,23 @@ const ContextRuleDraftSchema = zod_1.z
|
|
|
303
328
|
// existing four.
|
|
304
329
|
/** Pinned interpreter address (testnet).
|
|
305
330
|
* Single source for the MCP layer; do not embed elsewhere. */
|
|
306
|
-
exports.PINNED_INTERPRETER_TESTNET_ADDRESS = '
|
|
307
|
-
/** Pinned interpreter address (mainnet), redeployed 2026-08-22 from a reproducible build. The mainnet
|
|
331
|
+
exports.PINNED_INTERPRETER_TESTNET_ADDRESS = 'CCBHVZ6HGGV7C4SNHCZ3S5665Z2WEMHTMBAEPO4XW6PKON464BEBANU5';
|
|
332
|
+
/** Pinned interpreter address (mainnet), redeployed 2026-08-22 for grammar 4, from a reproducible build. The mainnet
|
|
308
333
|
* interpreter IS the binary exercised on testnet - both instances were created
|
|
309
334
|
* from the same uploaded wasm hash (see PINNED_INTERPRETER_WASM_SHA256), and
|
|
310
|
-
* both were read back with `grammar_version()` returning
|
|
335
|
+
* both were read back with `grammar_version()` returning 4. The address differs
|
|
311
336
|
* because instance ids are network-scoped. UNAUDITED at the time of writing.
|
|
312
337
|
*
|
|
313
338
|
* These four constants move together or not at all. The grammar version and
|
|
314
339
|
* wasm hash are single values covering BOTH networks, so re-pinning one network
|
|
315
340
|
* alone would have the builder emit a version the other network refuses - with
|
|
316
341
|
* a green test run, since `grammar-version-parity.test.ts` would then pass. */
|
|
317
|
-
exports.PINNED_INTERPRETER_MAINNET_ADDRESS = '
|
|
342
|
+
exports.PINNED_INTERPRETER_MAINNET_ADDRESS = 'CDN755TDYZM3ZQ5OXTJ6TIBUBWZV2KRI2BYJPBXD2MVWED4STT3VBN52';
|
|
318
343
|
/** Pinned interpreter wasm sha256 (hex). */
|
|
319
|
-
exports.PINNED_INTERPRETER_WASM_SHA256 = '
|
|
344
|
+
exports.PINNED_INTERPRETER_WASM_SHA256 = 'b5ba1e35ccf20cd8c13c3a2c3098bf337033a92bcaf475d63c03ddc0cba0fcae';
|
|
320
345
|
/** The grammar version the interpreter enforces (matches SELF_VERSION in
|
|
321
346
|
* contracts/policy-interpreter/src/version.rs). */
|
|
322
|
-
exports.PINNED_INTERPRETER_GRAMMAR_VERSION =
|
|
347
|
+
exports.PINNED_INTERPRETER_GRAMMAR_VERSION = 4;
|
|
323
348
|
/** Default Soroban RPC for the install / revoke / info tools. The recorder
|
|
324
349
|
* keeps its own copy in record/rpc.ts because it hands back a fetcher rather
|
|
325
350
|
* than a Server; the two are deliberately different surfaces, so this is
|
|
@@ -342,6 +367,33 @@ exports.RPC_URL_BY_NETWORK = {
|
|
|
342
367
|
testnet: exports.TESTNET_RPC_URL,
|
|
343
368
|
mainnet: exports.MAINNET_RPC_URL,
|
|
344
369
|
};
|
|
370
|
+
/** Instance addresses per network. Exported so consumers import the pin
|
|
371
|
+
* instead of copying a literal - a copied address is how a testnet id ends up
|
|
372
|
+
* being queried against mainnet, which returns `Error(Storage, MissingValue)`
|
|
373
|
+
* and reads exactly like "nothing is deployed there".
|
|
374
|
+
*
|
|
375
|
+
* Instance ids are network-scoped, so the addresses differ while the wasm
|
|
376
|
+
* hash does not: each pair below was created from the same uploaded wasm (see
|
|
377
|
+
* `PINNED_OZ_POLICY_WASM_SHA256`), verified by fetching the deployed bytes
|
|
378
|
+
* back from both networks. */
|
|
379
|
+
exports.PINNED_OZ_POLICY_ADDRESS_BY_NETWORK = {
|
|
380
|
+
testnet: {
|
|
381
|
+
spending_limit: 'CDH4KOBRUEZI6TTZ72YXR5YUIODB6RH3AF75KX56Z73DELRCA5TWFISP',
|
|
382
|
+
simple_threshold: 'CAYTIVQOEZDOQI4GC3XBXEEYHQUANQQJHPJVMXVRBREGSAP6TCN3DID6',
|
|
383
|
+
weighted_threshold: 'CCTNRFZCL45GTJICA3Z2KFQO3VEGBHGCVBLHQ3GLJKAGACQIJMYJS7T2',
|
|
384
|
+
},
|
|
385
|
+
mainnet: {
|
|
386
|
+
spending_limit: 'CA7IBD266HIHFDUIBZLPIAITJUA3DVY4JAG6K3QMGBKLZCXXLP5E2F7A',
|
|
387
|
+
simple_threshold: 'CDOGPGUFGGUDG25P3TG6XIXJKRRYOZ3PXUZIEPVH74KXRZIDKZ5HYEOS',
|
|
388
|
+
weighted_threshold: 'CDWPZ4YZ3YIJ64XSHRMERRF2L2H7XD6SPUZDP3KI56T7QXQCICC25V3J',
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
/** sha256 of each deployed policy wasm, identical across both networks. */
|
|
392
|
+
exports.PINNED_OZ_POLICY_WASM_SHA256 = {
|
|
393
|
+
spending_limit: '9ce30ea1fe5c2dc5c9c49cf3462adb32e2c11d7dfadb15ef43a51ba56568de2b',
|
|
394
|
+
simple_threshold: '01c0be09eb6fb288cab2e878b4e890f7a38f75afab99aeb197861f44e2e2dfe6',
|
|
395
|
+
weighted_threshold: '78030272b06afb09d2949ab8877c9a8ae1ab9025b48f4edafd5816cc44f76eaa',
|
|
396
|
+
};
|
|
345
397
|
/** Stellar network passphrases. Pinned here so the XDR envelope uses the
|
|
346
398
|
* matching passphrase when the wallet signs (a mismatch yields invalid
|
|
347
399
|
* hashes). */
|
|
@@ -382,6 +434,18 @@ exports.DeclarePolicyInputSchema = zod_1.z
|
|
|
382
434
|
recipients: zod_1.z.array(zod_1.z.string()).min(1, 'recipients must not be empty').optional(),
|
|
383
435
|
recipientArgIndex: zod_1.z.number().int().nonnegative().max(U32_MAX).optional(),
|
|
384
436
|
allowZeroCap: zod_1.z.boolean().optional(),
|
|
437
|
+
/** Minimum output as a ratio of the call's own input. num/den are decimal
|
|
438
|
+
* STRINGS for the same reason maxAmount is: an i128 ratio does not
|
|
439
|
+
* survive a JS number. */
|
|
440
|
+
minOutputRatio: zod_1.z
|
|
441
|
+
.object({
|
|
442
|
+
num: zod_1.z.string().regex(/^[0-9]+$/, 'num must be an unsigned integer'),
|
|
443
|
+
den: zod_1.z.string().regex(/^[0-9]+$/, 'den must be an unsigned integer'),
|
|
444
|
+
inputArgIndex: zod_1.z.number().int().nonnegative().max(U32_MAX),
|
|
445
|
+
outputArgIndex: zod_1.z.number().int().nonnegative().max(U32_MAX),
|
|
446
|
+
})
|
|
447
|
+
.strict()
|
|
448
|
+
.optional(),
|
|
385
449
|
})
|
|
386
450
|
.strict();
|
|
387
451
|
exports.InstallPolicyInputSchema = zod_1.z
|
|
@@ -205,11 +205,22 @@ function visit(node, facts) {
|
|
|
205
205
|
for (const child of node.children)
|
|
206
206
|
visit(child, facts);
|
|
207
207
|
return;
|
|
208
|
+
// NOT descended into. A deny case works by violating ONE constraint and
|
|
209
|
+
// asserting the predicate refuses the call. Violating one branch of an
|
|
210
|
+
// `or` proves nothing, because another branch can still permit, so the
|
|
211
|
+
// generated case would either fail or pass for the wrong reason. A sound
|
|
212
|
+
// deny case for a disjunction must violate EVERY branch at once, which
|
|
213
|
+
// this generator does not construct - so it emits none.
|
|
214
|
+
case 'or':
|
|
215
|
+
return;
|
|
208
216
|
case 'in':
|
|
209
217
|
facts.memberships.push(node);
|
|
210
218
|
return;
|
|
211
219
|
case 'eq':
|
|
220
|
+
case 'lt':
|
|
212
221
|
case 'lte':
|
|
222
|
+
case 'gt':
|
|
223
|
+
case 'gte':
|
|
213
224
|
facts.comparisons.push(node);
|
|
214
225
|
}
|
|
215
226
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// src/simulate/evaluate.ts - TypeScript reference evaluator for grammar version
|
|
2
|
+
// src/simulate/evaluate.ts - TypeScript reference evaluator for grammar version 4.
|
|
3
3
|
//
|
|
4
4
|
// Pure function. Determinism: same `(predicate, ctx)` -> byte-identical result,
|
|
5
5
|
// no clock, no randomness. Deny order (deny on FIRST violation, stable reason):
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
// 3. `in` membership; empty haystack ALWAYS denies -> 'NOT_IN_ALLOWLIST'
|
|
10
10
|
// 4. otherwise permit.
|
|
11
11
|
//
|
|
12
|
-
// Grammar version
|
|
13
|
-
// Grammar version
|
|
12
|
+
// Grammar version 4 nodes: and, or, eq, lt, lte, gt, gte, in
|
|
13
|
+
// Grammar version 4 leaves: call_contract, call_fn, call_arg(i),
|
|
14
14
|
// call_arg_len(i), call_arg_field(i, element, field),
|
|
15
|
+
// call_arg_scaled(i, num, den),
|
|
15
16
|
// literal_address, literal_i128, literal_symbol, literal_u32, literal_vec
|
|
16
|
-
// Grammar version
|
|
17
|
-
// UNSUPPORTED_NODE, NOT_IN_ALLOWLIST
|
|
17
|
+
// Grammar version 4 deny reasons: ARG_MISMATCH, CONTRACT_SCOPE,
|
|
18
|
+
// ARITHMETIC_OVERFLOW, UNSUPPORTED_NODE, NOT_IN_ALLOWLIST, SLIPPAGE_FLOOR
|
|
18
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
20
|
exports.evaluate = evaluate;
|
|
20
21
|
/** Evaluate a `PredicateNode` against the candidate call described by `ctx`.
|
|
@@ -36,8 +37,25 @@ function walk(node, ctx) {
|
|
|
36
37
|
}
|
|
37
38
|
return lastDeny ?? { permit: true };
|
|
38
39
|
}
|
|
40
|
+
// Permits on the first branch that holds; when none does, reports the
|
|
41
|
+
// FIRST branch's reason. Mirrors `Node::Or` in the Rust evaluator, which
|
|
42
|
+
// the conformance suite pins.
|
|
43
|
+
case 'or': {
|
|
44
|
+
let firstDeny = null;
|
|
45
|
+
for (const child of node.children) {
|
|
46
|
+
const r = walk(child, ctx);
|
|
47
|
+
if (r.permit)
|
|
48
|
+
return r;
|
|
49
|
+
if (firstDeny === null)
|
|
50
|
+
firstDeny = r;
|
|
51
|
+
}
|
|
52
|
+
return firstDeny ?? { permit: false, reason: 'UNSUPPORTED_NODE' };
|
|
53
|
+
}
|
|
39
54
|
case 'eq':
|
|
55
|
+
case 'lt':
|
|
40
56
|
case 'lte':
|
|
57
|
+
case 'gt':
|
|
58
|
+
case 'gte':
|
|
41
59
|
return evalCompare(node.op, node.left, node.right, ctx);
|
|
42
60
|
case 'in':
|
|
43
61
|
return evalIn(node.needle, node.haystack, ctx);
|
|
@@ -45,6 +63,16 @@ function walk(node, ctx) {
|
|
|
45
63
|
}
|
|
46
64
|
/** Comparison leaf evaluation. */
|
|
47
65
|
function evalCompare(op, left, right, ctx) {
|
|
66
|
+
// Scaled operands first, so the dedicated reasons reach the caller instead
|
|
67
|
+
// of a generic mismatch. Right-hand dispatch leads because
|
|
68
|
+
// `out >= in * num / den` is the canonical swap form. Mirrors the order in
|
|
69
|
+
// `eval_compare` on the Rust side.
|
|
70
|
+
if (right.kind === 'call_arg_scaled') {
|
|
71
|
+
return evalScaledCompare(op, left, right, true, ctx);
|
|
72
|
+
}
|
|
73
|
+
if (left.kind === 'call_arg_scaled') {
|
|
74
|
+
return evalScaledCompare(op, right, left, false, ctx);
|
|
75
|
+
}
|
|
48
76
|
// CONTRACT_SCOPE on call_contract eq
|
|
49
77
|
if (left.kind === 'call_contract' && op === 'eq') {
|
|
50
78
|
if (right.kind !== 'literal_address')
|
|
@@ -99,6 +127,53 @@ function evalCompare(op, left, right, ctx) {
|
|
|
99
127
|
// Unknown leaf/op combination - structural fail-closed.
|
|
100
128
|
return { permit: false, reason: 'UNSUPPORTED_NODE' };
|
|
101
129
|
}
|
|
130
|
+
/** i128 bounds. The contract computes in i128 and denies on overflow, so the
|
|
131
|
+
* reference has to draw the same line or the two layers disagree on inputs
|
|
132
|
+
* near the boundary. */
|
|
133
|
+
const I128_MIN = -(2n ** 127n);
|
|
134
|
+
const I128_MAX = 2n ** 127n - 1n;
|
|
135
|
+
/** Comparison where one side is `call_arg_scaled`. Mirrors
|
|
136
|
+
* `eval_scaled_arg_compare`: `args[index] * num / den` truncating toward
|
|
137
|
+
* zero, ARITHMETIC_OVERFLOW on arithmetic that does not fit or a zero
|
|
138
|
+
* denominator, SLIPPAGE_FLOOR on a comparison that simply fails. */
|
|
139
|
+
function evalScaledCompare(op, other, scaled, scaledOnRight, ctx) {
|
|
140
|
+
// Chaining two computed operands has no meaning a review card could state.
|
|
141
|
+
if (other.kind === 'call_arg_scaled')
|
|
142
|
+
return { permit: false, reason: 'UNSUPPORTED_NODE' };
|
|
143
|
+
// Could not READ the operand is a different failure from read-and-missed.
|
|
144
|
+
const input = argNumericBigInt(ctx.args[scaled.index]);
|
|
145
|
+
if (input === null)
|
|
146
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
147
|
+
let num;
|
|
148
|
+
let den;
|
|
149
|
+
try {
|
|
150
|
+
num = BigInt(scaled.num);
|
|
151
|
+
den = BigInt(scaled.den);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
155
|
+
}
|
|
156
|
+
if (den === 0n)
|
|
157
|
+
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
158
|
+
const product = input * num;
|
|
159
|
+
if (product < I128_MIN || product > I128_MAX) {
|
|
160
|
+
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
161
|
+
}
|
|
162
|
+
// BigInt division already truncates toward zero, matching i128 semantics.
|
|
163
|
+
const quotient = product / den;
|
|
164
|
+
if (quotient < I128_MIN || quotient > I128_MAX) {
|
|
165
|
+
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
166
|
+
}
|
|
167
|
+
const otherVal = other.kind === 'call_arg'
|
|
168
|
+
? argNumericBigInt(ctx.args[other.index])
|
|
169
|
+
: literalNumericBigInt(other);
|
|
170
|
+
if (otherVal === null)
|
|
171
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
172
|
+
const [a, b] = scaledOnRight ? [otherVal, quotient] : [quotient, otherVal];
|
|
173
|
+
return bigintCmp(op, a.toString(), b.toString())
|
|
174
|
+
? { permit: true }
|
|
175
|
+
: { permit: false, reason: 'SLIPPAGE_FLOOR' };
|
|
176
|
+
}
|
|
102
177
|
/** Per-ScVal equality. Handles literal_vec as an EXACT ordered sequence:
|
|
103
178
|
* compare element-by-element in order; deny if length or any element differs.
|
|
104
179
|
* Opaque args (`type: 'other'`) fail closed. */
|
|
@@ -268,7 +343,13 @@ function bigintCmp(op, aStr, bStr) {
|
|
|
268
343
|
switch (op) {
|
|
269
344
|
case 'eq':
|
|
270
345
|
return a === b;
|
|
346
|
+
case 'lt':
|
|
347
|
+
return a < b;
|
|
271
348
|
case 'lte':
|
|
272
349
|
return a <= b;
|
|
350
|
+
case 'gt':
|
|
351
|
+
return a > b;
|
|
352
|
+
case 'gte':
|
|
353
|
+
return a >= b;
|
|
273
354
|
}
|
|
274
355
|
}
|
|
@@ -18,6 +18,19 @@ export interface PolicyDeclaration {
|
|
|
18
18
|
* explicitly. A rule that permits nothing is a plausible thing to want and
|
|
19
19
|
* an implausible thing to want by accident. */
|
|
20
20
|
allowZeroCap?: boolean;
|
|
21
|
+
/** Minimum output as a ratio of the call's own input: the output argument
|
|
22
|
+
* must be at least `input * num / den`.
|
|
23
|
+
*
|
|
24
|
+
* A swap's acceptable output depends on the size of the trade, so a fixed
|
|
25
|
+
* floor would pin the policy to one trade size. The ratio is DECLARED, never
|
|
26
|
+
* inferred from a recording: a recorded rate is a price at one moment, and
|
|
27
|
+
* freezing it as policy would deny ordinary trades later. */
|
|
28
|
+
minOutputRatio?: {
|
|
29
|
+
num: string;
|
|
30
|
+
den: string;
|
|
31
|
+
inputArgIndex: number;
|
|
32
|
+
outputArgIndex: number;
|
|
33
|
+
};
|
|
21
34
|
}
|
|
22
35
|
export interface DeclaredPredicate {
|
|
23
36
|
predicate: PredicateNode;
|