@crediolabs/policy-synth 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/codegen/template.js +3 -3
- package/dist/record/decode.d.ts +6 -3
- package/dist/record/decode.js +56 -3
- package/dist/record/freshness.d.ts +14 -1
- package/dist/record/freshness.js +32 -2
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +25 -0
- package/dist/record/movements.d.ts +15 -3
- package/dist/record/movements.js +42 -7
- package/dist/run/index.d.ts +43 -0
- package/dist/run/index.js +222 -0
- package/dist/run/schemas.d.ts +2016 -0
- package/dist/run/schemas.js +223 -0
- package/dist/synth/address.d.ts +7 -0
- package/dist/synth/address.js +12 -0
- package/dist/synth/compose-from-recording.d.ts +4 -3
- package/dist/synth/compose-from-recording.js +16 -6
- package/dist/synth/deny-cases.d.ts +12 -2
- package/dist/synth/deny-cases.js +76 -4
- package/dist/synth/evaluate.js +2 -2
- package/dist/synth/index.d.ts +1 -0
- package/dist/synth/index.js +4 -0
- package/dist/synth/minimize.d.ts +1 -1
- package/dist/synth/minimize.js +3 -3
- package/dist/synth/synthesize-from-recording.d.ts +5 -2
- package/dist/synth/synthesize-from-recording.js +175 -69
- package/dist/types.d.ts +41 -1
- package/dist/types.js +17 -0
- package/dist/verify/simulate.js +18 -2
- package/dist/verify/verify.js +18 -2
- package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
- package/dist-cjs/adapters/interpreter/adapter.js +527 -0
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
- package/dist-cjs/adapters/interpreter/index.js +8 -0
- package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
- package/dist-cjs/adapters/oz/adapter.js +289 -0
- package/dist-cjs/adapters/oz/index.d.ts +1 -0
- package/dist-cjs/adapters/oz/index.js +8 -0
- package/dist-cjs/codegen/compile-gate.d.ts +33 -0
- package/dist-cjs/codegen/compile-gate.js +123 -0
- package/dist-cjs/codegen/index.d.ts +2 -0
- package/dist-cjs/codegen/index.js +14 -0
- package/dist-cjs/codegen/template.d.ts +18 -0
- package/dist-cjs/codegen/template.js +134 -0
- package/dist-cjs/errors.d.ts +37 -0
- package/dist-cjs/errors.js +3 -0
- package/dist-cjs/index.d.ts +11 -0
- package/dist-cjs/index.js +27 -0
- package/dist-cjs/ir/index.d.ts +1 -0
- package/dist-cjs/ir/index.js +3 -0
- package/dist-cjs/ir/types.d.ts +108 -0
- package/dist-cjs/ir/types.js +12 -0
- package/dist-cjs/mandate/index.d.ts +2 -0
- package/dist-cjs/mandate/index.js +6 -0
- package/dist-cjs/mandate/to-ir.d.ts +3 -0
- package/dist-cjs/mandate/to-ir.js +63 -0
- package/dist-cjs/mandate/types.d.ts +20 -0
- package/dist-cjs/mandate/types.js +9 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/predicate/encode.d.ts +10 -0
- package/dist-cjs/predicate/encode.js +252 -0
- package/dist-cjs/predicate/index.d.ts +1 -0
- package/dist-cjs/predicate/index.js +6 -0
- package/dist-cjs/record/decode.d.ts +79 -0
- package/dist-cjs/record/decode.js +441 -0
- package/dist-cjs/record/freshness.d.ts +30 -0
- package/dist-cjs/record/freshness.js +85 -0
- package/dist-cjs/record/index.d.ts +32 -0
- package/dist-cjs/record/index.js +191 -0
- package/dist-cjs/record/movements.d.ts +32 -0
- package/dist-cjs/record/movements.js +227 -0
- package/dist-cjs/record/rpc.d.ts +22 -0
- package/dist-cjs/record/rpc.js +74 -0
- package/dist-cjs/record/validate.d.ts +22 -0
- package/dist-cjs/record/validate.js +63 -0
- package/dist-cjs/registry/identify.d.ts +11 -0
- package/dist-cjs/registry/identify.js +87 -0
- package/dist-cjs/registry/index.d.ts +3 -0
- package/dist-cjs/registry/index.js +15 -0
- package/dist-cjs/registry/known-addresses.d.ts +16 -0
- package/dist-cjs/registry/known-addresses.js +53 -0
- package/dist-cjs/registry/protocols.d.ts +38 -0
- package/dist-cjs/registry/protocols.js +153 -0
- package/dist-cjs/review-card/builder.d.ts +14 -0
- package/dist-cjs/review-card/builder.js +264 -0
- package/dist-cjs/review-card/conflict.d.ts +40 -0
- package/dist-cjs/review-card/conflict.js +114 -0
- package/dist-cjs/review-card/cross-check.d.ts +11 -0
- package/dist-cjs/review-card/cross-check.js +151 -0
- package/dist-cjs/review-card/index.d.ts +3 -0
- package/dist-cjs/review-card/index.js +10 -0
- package/dist-cjs/run/index.d.ts +43 -0
- package/dist-cjs/run/index.js +237 -0
- package/dist-cjs/run/schemas.d.ts +2016 -0
- package/dist-cjs/run/schemas.js +226 -0
- package/dist-cjs/seams/index.d.ts +1 -0
- package/dist-cjs/seams/index.js +3 -0
- package/dist-cjs/seams/types.d.ts +66 -0
- package/dist-cjs/seams/types.js +12 -0
- package/dist-cjs/synth/address.d.ts +7 -0
- package/dist-cjs/synth/address.js +15 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +65 -0
- package/dist-cjs/synth/compose-from-recording.js +366 -0
- package/dist-cjs/synth/deny-cases.d.ts +22 -0
- package/dist-cjs/synth/deny-cases.js +438 -0
- package/dist-cjs/synth/evaluate.d.ts +39 -0
- package/dist-cjs/synth/evaluate.js +428 -0
- package/dist-cjs/synth/harness.d.ts +16 -0
- package/dist-cjs/synth/harness.js +29 -0
- package/dist-cjs/synth/index.d.ts +10 -0
- package/dist-cjs/synth/index.js +28 -0
- package/dist-cjs/synth/lower.d.ts +23 -0
- package/dist-cjs/synth/lower.js +119 -0
- package/dist-cjs/synth/minimize.d.ts +4 -0
- package/dist-cjs/synth/minimize.js +41 -0
- package/dist-cjs/synth/predicate-literals.d.ts +5 -0
- package/dist-cjs/synth/predicate-literals.js +28 -0
- package/dist-cjs/synth/scope.d.ts +26 -0
- package/dist-cjs/synth/scope.js +81 -0
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
- package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
- package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
- package/dist-cjs/synth/synthesize-from-recording.js +759 -0
- package/dist-cjs/types.d.ts +311 -0
- package/dist-cjs/types.js +55 -0
- package/dist-cjs/verify/envelope.d.ts +15 -0
- package/dist-cjs/verify/envelope.js +23 -0
- package/dist-cjs/verify/index.d.ts +3 -0
- package/dist-cjs/verify/index.js +8 -0
- package/dist-cjs/verify/simulate.d.ts +31 -0
- package/dist-cjs/verify/simulate.js +261 -0
- package/dist-cjs/verify/verify.d.ts +21 -0
- package/dist-cjs/verify/verify.js +192 -0
- package/package.json +43 -3
- package/src/codegen/template.ts +3 -3
- package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +171 -0
- package/src/record/corpus-fixtures.json +532 -0
- package/src/record/decode.ts +63 -10
- package/src/record/freshness.ts +34 -2
- package/src/record/index.ts +40 -0
- package/src/record/movements.ts +40 -7
- package/src/run/index.ts +277 -0
- package/src/run/schemas.ts +250 -0
- package/src/synth/address.ts +14 -0
- package/src/synth/compose-from-recording.ts +20 -9
- package/src/synth/deny-cases.ts +87 -4
- package/src/synth/evaluate.ts +2 -2
- package/src/synth/index.ts +4 -0
- package/src/synth/minimize.ts +7 -3
- package/src/synth/synthesize-from-recording.ts +200 -68
- package/src/types.ts +37 -1
- package/src/verify/simulate.ts +21 -2
- package/src/verify/verify.ts +21 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/registry/identify.ts - protocol identification (fail-closed).
|
|
3
|
+
//
|
|
4
|
+
// Given a (contract, method, args) triple, decide whether the call belongs
|
|
5
|
+
// to a known protocol. Returns null when it does NOT - the caller MUST
|
|
6
|
+
// preserve the fail-closed posture for null results.
|
|
7
|
+
//
|
|
8
|
+
// Recognition rules:
|
|
9
|
+
// 1. SEP-41: recognised by INTERFACE - any contract calling one of the
|
|
10
|
+
// SEP-41 fns (transfer / mint / burn / approve) with a matching arg
|
|
11
|
+
// shape is a SEP-41 token. No address pin is needed.
|
|
12
|
+
// 2. Blend: recognised by INTERFACE - real pool `submit` / `claim` calls
|
|
13
|
+
// hit per-pool instances, not the factory. The arg shape must match
|
|
14
|
+
// the Blend pool ABI (verified against the pool contract source).
|
|
15
|
+
// Address recognition still applies when the call is against the
|
|
16
|
+
// pinned factory address, with the same arg-shape check.
|
|
17
|
+
// 3. SoroSwap: recognised by ADDRESS only - the router is a single pinned
|
|
18
|
+
// contract. FIX 4: the method must ALSO be present in the protocol's
|
|
19
|
+
// ABI; an unknown method on a pinned router -> null (fail-closed).
|
|
20
|
+
//
|
|
21
|
+
// Method lookup uses `Object.hasOwn` rather than `in` so prototype-chain
|
|
22
|
+
// names like `constructor`, `toString`, `hasOwnProperty` do NOT register as
|
|
23
|
+
// valid method calls. Arg-shape validation compares arg count + each arg's
|
|
24
|
+
// ScVal subset type against the ABI signature; mismatch -> null.
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.identifyProtocol = identifyProtocol;
|
|
27
|
+
const known_addresses_ts_1 = require("./known-addresses.js");
|
|
28
|
+
const protocols_ts_1 = require("./protocols.js");
|
|
29
|
+
/** Identify the protocol for a single (contract, method, args) invocation.
|
|
30
|
+
* Returns null on ANY mismatch (method not in ABI, arg count off, arg type
|
|
31
|
+
* off, or unknown method on a pinned address). The caller MUST keep the
|
|
32
|
+
* null path fail-closed. */
|
|
33
|
+
function identifyProtocol(contract, method, args, network) {
|
|
34
|
+
// 1) SEP-41 by interface. Any contract whose (method, args) matches a
|
|
35
|
+
// SEP-41 ABI signature is a SEP-41 token.
|
|
36
|
+
if (Object.hasOwn(protocols_ts_1.SEP41_ABI, method)) {
|
|
37
|
+
const sig = protocols_ts_1.SEP41_ABI[method];
|
|
38
|
+
if (sig && argsMatchAbi(sig.args, args)) {
|
|
39
|
+
return { protocol: 'sep41', fn: method };
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// 2) Blend by interface. Real pool calls hit per-pool instances; pinned
|
|
44
|
+
// factory calls also match here when the method is in the pool ABI.
|
|
45
|
+
if (Object.hasOwn(protocols_ts_1.BLEND_ABI, method)) {
|
|
46
|
+
const sig = protocols_ts_1.BLEND_ABI[method];
|
|
47
|
+
if (sig && argsMatchAbi(sig.args, args)) {
|
|
48
|
+
return { protocol: 'blend', fn: method };
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
// 3) Pinned-address recognition (Blend factory + SoroSwap router/factory).
|
|
53
|
+
// FIX 4: the method must also be in the protocol's ABI; an unknown
|
|
54
|
+
// method on a pinned address -> null.
|
|
55
|
+
if (network) {
|
|
56
|
+
const pinned = (0, known_addresses_ts_1.addressToProtocol)(contract, network);
|
|
57
|
+
if (pinned) {
|
|
58
|
+
const abi = pinned === 'blend' ? protocols_ts_1.BLEND_ABI : protocols_ts_1.SOROSWAP_ABI;
|
|
59
|
+
if (Object.hasOwn(abi, method)) {
|
|
60
|
+
const sig = abi[method];
|
|
61
|
+
if (sig && argsMatchAbi(sig.args, args)) {
|
|
62
|
+
return { protocol: pinned, fn: method };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
void contract;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/** Compare the decoded args against the ABI signature. Returns true only when
|
|
71
|
+
* the arg count matches AND every arg's ScVal subset type matches the ABI's
|
|
72
|
+
* declared type. `other` is intentionally NOT a valid ABI match - it means
|
|
73
|
+
* the decoder couldn't classify the value, which is exactly the signal
|
|
74
|
+
* fail-closed should refuse. */
|
|
75
|
+
function argsMatchAbi(expected, actual) {
|
|
76
|
+
if (expected.length !== actual.length)
|
|
77
|
+
return false;
|
|
78
|
+
for (let i = 0; i < expected.length; i += 1) {
|
|
79
|
+
const want = expected[i];
|
|
80
|
+
const got = actual[i];
|
|
81
|
+
if (!want || !got)
|
|
82
|
+
return false;
|
|
83
|
+
if (want.type !== got.type)
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { type IdentifiedProtocol, identifyProtocol, } from './identify.ts';
|
|
2
|
+
export { addressToProtocol, KNOWN_ADDRESSES, type KnownAddressesByNetwork, type KnownAddressesByProtocol, } from './known-addresses.ts';
|
|
3
|
+
export { type AbiArg, type AbiEntry, BLEND_ABI, getAbi, PROTOCOL_ABIS, type ProtocolAbi, type ProtocolId, SEP41_ABI, SOROSWAP_ABI, } from './protocols.ts';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/registry/index.ts - re-export the protocol adjacency registry.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.SOROSWAP_ABI = exports.SEP41_ABI = exports.PROTOCOL_ABIS = exports.getAbi = exports.BLEND_ABI = exports.KNOWN_ADDRESSES = exports.addressToProtocol = exports.identifyProtocol = void 0;
|
|
5
|
+
var identify_ts_1 = require("./identify.js");
|
|
6
|
+
Object.defineProperty(exports, "identifyProtocol", { enumerable: true, get: function () { return identify_ts_1.identifyProtocol; } });
|
|
7
|
+
var known_addresses_ts_1 = require("./known-addresses.js");
|
|
8
|
+
Object.defineProperty(exports, "addressToProtocol", { enumerable: true, get: function () { return known_addresses_ts_1.addressToProtocol; } });
|
|
9
|
+
Object.defineProperty(exports, "KNOWN_ADDRESSES", { enumerable: true, get: function () { return known_addresses_ts_1.KNOWN_ADDRESSES; } });
|
|
10
|
+
var protocols_ts_1 = require("./protocols.js");
|
|
11
|
+
Object.defineProperty(exports, "BLEND_ABI", { enumerable: true, get: function () { return protocols_ts_1.BLEND_ABI; } });
|
|
12
|
+
Object.defineProperty(exports, "getAbi", { enumerable: true, get: function () { return protocols_ts_1.getAbi; } });
|
|
13
|
+
Object.defineProperty(exports, "PROTOCOL_ABIS", { enumerable: true, get: function () { return protocols_ts_1.PROTOCOL_ABIS; } });
|
|
14
|
+
Object.defineProperty(exports, "SEP41_ABI", { enumerable: true, get: function () { return protocols_ts_1.SEP41_ABI; } });
|
|
15
|
+
Object.defineProperty(exports, "SOROSWAP_ABI", { enumerable: true, get: function () { return protocols_ts_1.SOROSWAP_ABI; } });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ProtocolId } from './protocols.ts';
|
|
2
|
+
/** Pinned contract addresses per (network, protocol). Empty list = no
|
|
3
|
+
* addresses pinned for that cell. */
|
|
4
|
+
export interface KnownAddressesByProtocol {
|
|
5
|
+
blend: string[];
|
|
6
|
+
soroswap: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface KnownAddressesByNetwork {
|
|
9
|
+
mainnet: KnownAddressesByProtocol;
|
|
10
|
+
testnet: KnownAddressesByProtocol;
|
|
11
|
+
}
|
|
12
|
+
export declare const KNOWN_ADDRESSES: KnownAddressesByNetwork;
|
|
13
|
+
/** Map a (contract, network) pair to its pinned protocol id. Returns null
|
|
14
|
+
* when the address is not pinned for that network - callers MUST preserve
|
|
15
|
+
* the fail-closed posture for unpinned contracts. */
|
|
16
|
+
export declare function addressToProtocol(contract: string, network: 'mainnet' | 'testnet'): ProtocolId | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/registry/known-addresses.ts - pinned real protocol contract addresses.
|
|
3
|
+
//
|
|
4
|
+
// Addresses here are fetched from authoritative public sources:
|
|
5
|
+
// - Blend mainnet: docs.blend.capital/mainnet-deployments
|
|
6
|
+
// https://docs.blend.capital/mainnet-deployments
|
|
7
|
+
// - SoroSwap mainnet: github.com/soroswap (organisation README)
|
|
8
|
+
// https://github.com/soroswap
|
|
9
|
+
//
|
|
10
|
+
// Testnet addresses are intentionally NOT pinned: the public docs do not
|
|
11
|
+
// publish them, and per the task brief, unconfirmed addresses are marked
|
|
12
|
+
// [VERIFY] and excluded from the active set rather than invented.
|
|
13
|
+
//
|
|
14
|
+
// SEP-41 is recognised by interface (any token), so no address list is needed
|
|
15
|
+
// for it - see identify.ts.
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.KNOWN_ADDRESSES = void 0;
|
|
18
|
+
exports.addressToProtocol = addressToProtocol;
|
|
19
|
+
// Source: https://docs.blend.capital/mainnet-deployments (Blend Pool Factory
|
|
20
|
+
// mainnet deployment, fetched 2026-07-23).
|
|
21
|
+
const BLEND_MAINNET_POOL_FACTORY = 'CDSYOAVXFY7SM5S64IZPPPYB4GVGGLMQVFREPSQQEZVIWXX5R23G4QSU';
|
|
22
|
+
// Source: https://github.com/soroswap (organisation README, fetched 2026-07-23)
|
|
23
|
+
// SoroswapRouter mainnet deployment.
|
|
24
|
+
const SOROSWAP_MAINNET_ROUTER = 'CAG5LRYQ5JVEUI5TEID72EYOVX44TTUJT5BQR2J6J77FH65PCCFAJDDH';
|
|
25
|
+
// Source: https://github.com/soroswap (organisation README, fetched 2026-07-23)
|
|
26
|
+
// SoroswapFactory mainnet deployment.
|
|
27
|
+
const SOROSWAP_MAINNET_FACTORY = 'CA4HEQTL2WPEUYKYKCDOHCDNIV4QHNJ7EL4J4NQ6VADP7SYHVRYZ7AW2';
|
|
28
|
+
exports.KNOWN_ADDRESSES = {
|
|
29
|
+
mainnet: {
|
|
30
|
+
blend: [BLEND_MAINNET_POOL_FACTORY],
|
|
31
|
+
soroswap: [SOROSWAP_MAINNET_ROUTER, SOROSWAP_MAINNET_FACTORY],
|
|
32
|
+
},
|
|
33
|
+
testnet: {
|
|
34
|
+
// [VERIFY] Blend testnet pool factory address: not published in the
|
|
35
|
+
// public docs as of 2026-07-23. Excluded from the active set until an
|
|
36
|
+
// authoritative source is found.
|
|
37
|
+
blend: [],
|
|
38
|
+
// [VERIFY] SoroSwap testnet router / factory addresses: not published in
|
|
39
|
+
// the public docs as of 2026-07-23. Excluded from the active set.
|
|
40
|
+
soroswap: [],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
/** Map a (contract, network) pair to its pinned protocol id. Returns null
|
|
44
|
+
* when the address is not pinned for that network - callers MUST preserve
|
|
45
|
+
* the fail-closed posture for unpinned contracts. */
|
|
46
|
+
function addressToProtocol(contract, network) {
|
|
47
|
+
for (const protocol of ['blend', 'soroswap']) {
|
|
48
|
+
if (exports.KNOWN_ADDRESSES[network][protocol].includes(contract)) {
|
|
49
|
+
return protocol;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ScVal } from '../types.ts';
|
|
2
|
+
export type ProtocolId = 'sep41' | 'blend' | 'soroswap';
|
|
3
|
+
/** ScVal subset type vocabulary the ABI uses. Mirrors `ScVal['type']` minus
|
|
4
|
+
* `other` (which by definition cannot be matched against an ABI arg). */
|
|
5
|
+
export type AbiArgType = Exclude<ScVal['type'], 'other'>;
|
|
6
|
+
export interface AbiArg {
|
|
7
|
+
name: string;
|
|
8
|
+
/** ScVal subset type the decoded arg must conform to. */
|
|
9
|
+
type: AbiArgType;
|
|
10
|
+
/** Human-readable meaning for the downstream reviewer. */
|
|
11
|
+
meaning: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AbiEntry {
|
|
14
|
+
args: AbiArg[];
|
|
15
|
+
}
|
|
16
|
+
export type ProtocolAbi = Record<string, AbiEntry>;
|
|
17
|
+
/** SEP-41 token interface - pinned from the SEP-41 spec (any token). */
|
|
18
|
+
export declare const SEP41_ABI: ProtocolAbi;
|
|
19
|
+
/** Blend pool interface (v2). Pinning the two fns the recorder surfaces.
|
|
20
|
+
* Pool-level recognition is by INTERFACE (real pool calls hit per-pool
|
|
21
|
+
* instances, not the factory). Arg shapes verified against the Blend v2
|
|
22
|
+
* pool contract source (blend-capital/blend-contracts-v2 `pool/src/contract.rs`,
|
|
23
|
+
* fetched 2026-07-23):
|
|
24
|
+
* submit(env, from: Address, spender: Address, to: Address, requests: Vec<Request>)
|
|
25
|
+
* -> Positions
|
|
26
|
+
* claim(env, from: Address, reserve_token_ids: Vec<u32>, to: Address) -> i128
|
|
27
|
+
* Other pool fns (add_reserves, withdraw, etc.) are intentionally omitted -
|
|
28
|
+
* they are too version-sensitive to bind by interface. */
|
|
29
|
+
export declare const BLEND_ABI: ProtocolAbi;
|
|
30
|
+
/** SoroSwap router interface. Standard Uniswap-V2-style swap fn signatures
|
|
31
|
+
* pinned from the SoroSwap router source. Recognised by address only (the
|
|
32
|
+
* router is a single pinned contract); FIX 4 requires the method to also be
|
|
33
|
+
* in this ABI for address recognition to succeed. The `path` arg is a
|
|
34
|
+
* `vec<address>` - we record it as the outer `vec` subset and leave the
|
|
35
|
+
* per-element shape to the downstream reviewer. */
|
|
36
|
+
export declare const SOROSWAP_ABI: ProtocolAbi;
|
|
37
|
+
export declare const PROTOCOL_ABIS: Record<ProtocolId, ProtocolAbi>;
|
|
38
|
+
export declare function getAbi(protocol: ProtocolId): ProtocolAbi;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/registry/protocols.ts - protocol ABI definitions (no addresses).
|
|
3
|
+
//
|
|
4
|
+
// A protocol is identified by EITHER:
|
|
5
|
+
// - by interface (function-name signature + arg shape): a contract calling
|
|
6
|
+
// one of these well-known fns with the expected arg shape IS that
|
|
7
|
+
// protocol, regardless of address. SEP-41 is the canonical case - ANY
|
|
8
|
+
// token contract calling `transfer` with the right arg shape is a
|
|
9
|
+
// SEP-41 token. Blend pool `submit`/`claim` are similarly recognised by
|
|
10
|
+
// interface (the pool factory address doesn't cover per-pool instances).
|
|
11
|
+
// - by address: the contract is in the pinned `known-addresses` set (see
|
|
12
|
+
// known-addresses.ts). SoroSwap uses address recognition because the
|
|
13
|
+
// router is a single deployed contract; Blend uses it to recognise factory
|
|
14
|
+
// calls in addition to its interface recognition.
|
|
15
|
+
//
|
|
16
|
+
// Args entries pin a ScVal subset type (matching the normalised `ScVal`
|
|
17
|
+
// vocabulary in src/types.ts) plus a human meaning string. The registry
|
|
18
|
+
// fails closed: a method call whose decoded args do NOT match the ABI
|
|
19
|
+
// signature is treated as unknown.
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.PROTOCOL_ABIS = exports.SOROSWAP_ABI = exports.BLEND_ABI = exports.SEP41_ABI = void 0;
|
|
22
|
+
exports.getAbi = getAbi;
|
|
23
|
+
/** SEP-41 token interface - pinned from the SEP-41 spec (any token). */
|
|
24
|
+
exports.SEP41_ABI = {
|
|
25
|
+
transfer: {
|
|
26
|
+
args: [
|
|
27
|
+
{
|
|
28
|
+
name: 'from',
|
|
29
|
+
type: 'address',
|
|
30
|
+
meaning: 'source address (the `from` of the token transfer)',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'to',
|
|
34
|
+
type: 'address',
|
|
35
|
+
meaning: 'destination address (the `to` of the token transfer)',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'amount',
|
|
39
|
+
type: 'i128',
|
|
40
|
+
meaning: 'amount of tokens to move (i128, signed-decimal string)',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
mint: {
|
|
45
|
+
args: [
|
|
46
|
+
{ name: 'to', type: 'address', meaning: 'recipient of the newly minted tokens' },
|
|
47
|
+
{ name: 'amount', type: 'i128', meaning: 'amount to mint (i128, signed-decimal string)' },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
burn: {
|
|
51
|
+
args: [
|
|
52
|
+
{ name: 'from', type: 'address', meaning: 'address whose balance is decremented' },
|
|
53
|
+
{ name: 'amount', type: 'i128', meaning: 'amount to burn (i128, signed-decimal string)' },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
approve: {
|
|
57
|
+
args: [
|
|
58
|
+
{ name: 'from', type: 'address', meaning: 'token holder granting the allowance' },
|
|
59
|
+
{
|
|
60
|
+
name: 'spender',
|
|
61
|
+
type: 'address',
|
|
62
|
+
meaning: 'address authorised to move tokens on behalf of `from`',
|
|
63
|
+
},
|
|
64
|
+
{ name: 'amount', type: 'i128', meaning: 'allowance amount (i128, signed-decimal string)' },
|
|
65
|
+
{
|
|
66
|
+
name: 'expiration_ledger',
|
|
67
|
+
type: 'u32',
|
|
68
|
+
meaning: 'ledger sequence after which the allowance is invalid',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
/** Blend pool interface (v2). Pinning the two fns the recorder surfaces.
|
|
74
|
+
* Pool-level recognition is by INTERFACE (real pool calls hit per-pool
|
|
75
|
+
* instances, not the factory). Arg shapes verified against the Blend v2
|
|
76
|
+
* pool contract source (blend-capital/blend-contracts-v2 `pool/src/contract.rs`,
|
|
77
|
+
* fetched 2026-07-23):
|
|
78
|
+
* submit(env, from: Address, spender: Address, to: Address, requests: Vec<Request>)
|
|
79
|
+
* -> Positions
|
|
80
|
+
* claim(env, from: Address, reserve_token_ids: Vec<u32>, to: Address) -> i128
|
|
81
|
+
* Other pool fns (add_reserves, withdraw, etc.) are intentionally omitted -
|
|
82
|
+
* they are too version-sensitive to bind by interface. */
|
|
83
|
+
exports.BLEND_ABI = {
|
|
84
|
+
submit: {
|
|
85
|
+
args: [
|
|
86
|
+
{ name: 'from', type: 'address', meaning: 'address supplying collateral or repaying debt' },
|
|
87
|
+
{ name: 'spender', type: 'address', meaning: 'address authorising the supply / repay' },
|
|
88
|
+
{ name: 'to', type: 'address', meaning: 'address receiving the resulting position shares' },
|
|
89
|
+
{ name: 'requests', type: 'vec', meaning: 'vec<Request> describing the action per reserve' },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
claim: {
|
|
93
|
+
args: [
|
|
94
|
+
{ name: 'from', type: 'address', meaning: 'address whose position is being claimed against' },
|
|
95
|
+
{
|
|
96
|
+
name: 'reserve_token_ids',
|
|
97
|
+
type: 'vec',
|
|
98
|
+
meaning: 'vec<u32> reserve token ids to claim emissions for',
|
|
99
|
+
},
|
|
100
|
+
{ name: 'to', type: 'address', meaning: 'address receiving the claimed tokens' },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
/** SoroSwap router interface. Standard Uniswap-V2-style swap fn signatures
|
|
105
|
+
* pinned from the SoroSwap router source. Recognised by address only (the
|
|
106
|
+
* router is a single pinned contract); FIX 4 requires the method to also be
|
|
107
|
+
* in this ABI for address recognition to succeed. The `path` arg is a
|
|
108
|
+
* `vec<address>` - we record it as the outer `vec` subset and leave the
|
|
109
|
+
* per-element shape to the downstream reviewer. */
|
|
110
|
+
exports.SOROSWAP_ABI = {
|
|
111
|
+
swap_exact_tokens_for_tokens: {
|
|
112
|
+
args: [
|
|
113
|
+
{ name: 'amount_in', type: 'i128', meaning: 'exact input amount (i128)' },
|
|
114
|
+
{ name: 'amount_out_min', type: 'i128', meaning: 'minimum acceptable output (i128)' },
|
|
115
|
+
{ name: 'path', type: 'vec', meaning: 'vec<address> hop path through pools' },
|
|
116
|
+
{ name: 'to', type: 'address', meaning: 'recipient of the output tokens' },
|
|
117
|
+
{
|
|
118
|
+
name: 'deadline',
|
|
119
|
+
type: 'u64',
|
|
120
|
+
meaning: 'unix-seconds deadline after which the swap is refused',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
swap_tokens_for_exact_tokens: {
|
|
125
|
+
args: [
|
|
126
|
+
{ name: 'amount_out', type: 'i128', meaning: 'exact output amount desired (i128)' },
|
|
127
|
+
{ name: 'amount_in_max', type: 'i128', meaning: 'maximum input willing to spend (i128)' },
|
|
128
|
+
{ name: 'path', type: 'vec', meaning: 'vec<address> hop path through pools' },
|
|
129
|
+
{ name: 'to', type: 'address', meaning: 'recipient of the output tokens' },
|
|
130
|
+
{
|
|
131
|
+
name: 'deadline',
|
|
132
|
+
type: 'u64',
|
|
133
|
+
meaning: 'unix-seconds deadline after which the swap is refused',
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
swap_exact_in_for_tokens: {
|
|
138
|
+
args: [
|
|
139
|
+
{ name: 'amount_in', type: 'i128', meaning: 'exact input amount (i128)' },
|
|
140
|
+
{ name: 'amount_out_min', type: 'i128', meaning: 'minimum acceptable output (i128)' },
|
|
141
|
+
{ name: 'path', type: 'vec', meaning: 'vec<address> hop path through pools' },
|
|
142
|
+
{ name: 'to', type: 'address', meaning: 'recipient of the output tokens' },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
exports.PROTOCOL_ABIS = {
|
|
147
|
+
sep41: exports.SEP41_ABI,
|
|
148
|
+
blend: exports.BLEND_ABI,
|
|
149
|
+
soroswap: exports.SOROSWAP_ABI,
|
|
150
|
+
};
|
|
151
|
+
function getAbi(protocol) {
|
|
152
|
+
return exports.PROTOCOL_ABIS[protocol];
|
|
153
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ContextRuleDraft, PolicyRef, PredicateNode } from '../types.ts';
|
|
2
|
+
import type { SimulationResult } from '../verify/envelope.ts';
|
|
3
|
+
export interface ReviewCardSummary {
|
|
4
|
+
ruleName: string;
|
|
5
|
+
plainEnglish: string;
|
|
6
|
+
constraints: string[];
|
|
7
|
+
expiry: string;
|
|
8
|
+
backend: 'interpreter-v1' | 'ts-model';
|
|
9
|
+
/** Stable hash of the builder inputs - identical policy + summary = identical hash. */
|
|
10
|
+
contentHash: string;
|
|
11
|
+
}
|
|
12
|
+
/** Build a deterministic review-card summary from a policy + context rule +
|
|
13
|
+
* simulation result. Pure: same inputs -> byte-identical output. */
|
|
14
|
+
export declare function buildReviewCardSummary(predicate: PredicateNode | null, policyRefs: PolicyRef[], contextRule: ContextRuleDraft, simulation: SimulationResult): ReviewCardSummary;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/review-card/builder.ts - deterministic, pure review-card summary
|
|
3
|
+
// builder.
|
|
4
|
+
//
|
|
5
|
+
// `buildReviewCardSummary` renders the per-policy text the user-facing review
|
|
6
|
+
// card quotes. The whole DX win of non-engineer reviewability hinges on the
|
|
7
|
+
// summary being REPRODUCIBLE + TESTABLE + NON-HALLUCINABLE, so this module is
|
|
8
|
+
// pure: same inputs -> byte-identical output, no clock, no randomness, no I/O.
|
|
9
|
+
//
|
|
10
|
+
// The builder walks two inputs and emits ONE constraint string per leaf or
|
|
11
|
+
// primitive, in a fixed deterministic order:
|
|
12
|
+
//
|
|
13
|
+
// 1. The OZ built-in `PolicyRef`s. Each `spending_limit` primitive becomes a
|
|
14
|
+
// `spending_limit(token, limitAmount, windowSecs)` line; other OZ
|
|
15
|
+
// primitives (threshold) are skipped - the review card does not quote
|
|
16
|
+
// them (they are signer-config concerns, not transactional bounds).
|
|
17
|
+
//
|
|
18
|
+
// 2. The interpreter `PredicateNode`. One string per constraint leaf,
|
|
19
|
+
// rendered by enclosing-comparison kind. Templates (Task 7b):
|
|
20
|
+
// - invocation_count_in_window <= N -> Invocations <= N per <window> seconds
|
|
21
|
+
// - call_arg[i] in [list] -> Recipient/arg must be one of [list]
|
|
22
|
+
// - eq(call_arg[i], literal_vec[...]) -> Path must be exactly [list]
|
|
23
|
+
// - oracle_price(asset) OP price -> Only when oracle_price(asset) OP price
|
|
24
|
+
// - call_fn == x -> Function must be x
|
|
25
|
+
// - call_contract == c -> Contract must be c
|
|
26
|
+
// - amount <= v -> Amount <= v
|
|
27
|
+
//
|
|
28
|
+
// The content hash is a stable sha256 hex of a canonical JSON of
|
|
29
|
+
// { ruleName, plainEnglish, constraints, expiry, backend } - identical
|
|
30
|
+
// inputs (incl. the context-rule expiry and simulation backend) -> identical
|
|
31
|
+
// hash. There is no clock; the hash never includes a timestamp.
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.buildReviewCardSummary = buildReviewCardSummary;
|
|
34
|
+
const node_crypto_1 = require("node:crypto");
|
|
35
|
+
/** Build a deterministic review-card summary from a policy + context rule +
|
|
36
|
+
* simulation result. Pure: same inputs -> byte-identical output. */
|
|
37
|
+
function buildReviewCardSummary(predicate, policyRefs, contextRule, simulation) {
|
|
38
|
+
const constraints = [];
|
|
39
|
+
for (const ref of policyRefs) {
|
|
40
|
+
const line = renderOzPrimitive(ref);
|
|
41
|
+
if (line !== null)
|
|
42
|
+
constraints.push(line);
|
|
43
|
+
}
|
|
44
|
+
if (predicate !== null) {
|
|
45
|
+
walkPredicate(predicate, (node) => {
|
|
46
|
+
const line = renderConstraint(node);
|
|
47
|
+
if (line !== null)
|
|
48
|
+
constraints.push(line);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const ruleName = contextRule.name;
|
|
52
|
+
const plainEnglish = renderPlainEnglish(ruleName, constraints);
|
|
53
|
+
const expiry = renderExpiry(contextRule.validUntilLedger);
|
|
54
|
+
const backend = simulation.backend;
|
|
55
|
+
const contentHash = computeContentHash({
|
|
56
|
+
ruleName,
|
|
57
|
+
plainEnglish,
|
|
58
|
+
constraints,
|
|
59
|
+
expiry,
|
|
60
|
+
backend,
|
|
61
|
+
});
|
|
62
|
+
return { ruleName, plainEnglish, constraints, expiry, backend, contentHash };
|
|
63
|
+
}
|
|
64
|
+
/** Render the OZ built-in primitive summary line. Only `spending_limit` is
|
|
65
|
+
* quoted by the review card (it is the only primitive that defines a
|
|
66
|
+
* transactional bound). Other primitives (threshold) are signer-config
|
|
67
|
+
* concerns handled by the OZ adapter's own `uncovered` machinery.
|
|
68
|
+
* Spending_limit takes `period_ledgers` on-chain (~5s/ledger); the card
|
|
69
|
+
* states the window in seconds so the user reads it consistently with the
|
|
70
|
+
* interpreter templates. */
|
|
71
|
+
function renderOzPrimitive(ref) {
|
|
72
|
+
if (ref.kind !== 'oz_builtin')
|
|
73
|
+
return null;
|
|
74
|
+
const primitive = ref.primitive;
|
|
75
|
+
if (primitive.primitive !== 'spending_limit')
|
|
76
|
+
return null;
|
|
77
|
+
const params = primitive.params;
|
|
78
|
+
const limit = params.spending_limit ?? '0';
|
|
79
|
+
const periodLedgers = params.period_ledgers ?? 0;
|
|
80
|
+
const windowSecs = periodLedgers * 5;
|
|
81
|
+
return `spending_limit(${limit}, ${windowSecs})`;
|
|
82
|
+
}
|
|
83
|
+
/** Walk every comparison / membership node of the predicate and invoke
|
|
84
|
+
* `visit` on each. The walk is depth-first, left-to-right, so the
|
|
85
|
+
* constraint list is stable across runs. Pure boolean nodes contribute no
|
|
86
|
+
* constraint lines themselves; their leaf children do, via the visitor. */
|
|
87
|
+
function walkPredicate(node, visit) {
|
|
88
|
+
switch (node.op) {
|
|
89
|
+
case 'and':
|
|
90
|
+
case 'or':
|
|
91
|
+
for (const child of node.children)
|
|
92
|
+
walkPredicate(child, visit);
|
|
93
|
+
return;
|
|
94
|
+
case 'not':
|
|
95
|
+
walkPredicate(node.child, visit);
|
|
96
|
+
return;
|
|
97
|
+
case 'in':
|
|
98
|
+
visit(node);
|
|
99
|
+
return;
|
|
100
|
+
case 'eq':
|
|
101
|
+
case 'lt':
|
|
102
|
+
case 'lte':
|
|
103
|
+
case 'gt':
|
|
104
|
+
case 'gte':
|
|
105
|
+
visit(node);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Render ONE constraint sentence for ONE interpreter predicate node. The
|
|
110
|
+
* shape of the output is pinned by Task 7b so the test suite can assert
|
|
111
|
+
* byte-for-byte equality. Returns `null` when the node is a structural
|
|
112
|
+
* boolean (`and` / `or` / `not`) - those are not constraint leaves. */
|
|
113
|
+
function renderConstraint(node) {
|
|
114
|
+
switch (node.op) {
|
|
115
|
+
case 'and':
|
|
116
|
+
case 'or':
|
|
117
|
+
case 'not':
|
|
118
|
+
return null;
|
|
119
|
+
case 'eq':
|
|
120
|
+
case 'lt':
|
|
121
|
+
case 'lte':
|
|
122
|
+
case 'gt':
|
|
123
|
+
case 'gte':
|
|
124
|
+
return renderComparison(node);
|
|
125
|
+
case 'in':
|
|
126
|
+
return renderMembership(node);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function renderComparison(node) {
|
|
130
|
+
const left = node.left;
|
|
131
|
+
const right = node.right;
|
|
132
|
+
// eq(call_contract, literal_address) -> Contract must be <addr>
|
|
133
|
+
if (left.kind === 'call_contract' && node.op === 'eq' && right.kind === 'literal_address') {
|
|
134
|
+
return `Contract must be ${right.value}`;
|
|
135
|
+
}
|
|
136
|
+
// eq(call_fn, literal_symbol) -> Function must be <sym>
|
|
137
|
+
if (left.kind === 'call_fn' && node.op === 'eq' && right.kind === 'literal_symbol') {
|
|
138
|
+
return `Function must be ${right.value}`;
|
|
139
|
+
}
|
|
140
|
+
// eq(call_arg[i], literal_vec) -> Path must be exactly [list]
|
|
141
|
+
if (left.kind === 'call_arg' && node.op === 'eq' && right.kind === 'literal_vec') {
|
|
142
|
+
return `Path must be exactly [${right.elements.map(renderVecElement).join(', ')}]`;
|
|
143
|
+
}
|
|
144
|
+
// invocation_count_in_window <= N -> Invocations <= N per <window> seconds
|
|
145
|
+
if (left.kind === 'invocation_count_in_window' && right.kind === 'literal_u32') {
|
|
146
|
+
return `Invocations <= ${right.value} per ${left.windowSecs} seconds`;
|
|
147
|
+
}
|
|
148
|
+
// amount <= v -> Amount <= v
|
|
149
|
+
if (left.kind === 'amount' && right.kind === 'literal_i128') {
|
|
150
|
+
return `Amount <= ${right.value}`;
|
|
151
|
+
}
|
|
152
|
+
// oracle_price(asset) OP price -> Only when oracle_price(asset) OP price
|
|
153
|
+
if (left.kind === 'oracle_price' && right.kind === 'literal_i128') {
|
|
154
|
+
return `Only when oracle_price(${left.asset}) ${comparisonOpText(node.op)} ${right.value}`;
|
|
155
|
+
}
|
|
156
|
+
// Any other comparison shape is a structural fail-closed: do not surface
|
|
157
|
+
// a misleading line. Cross-check still requires every leaf produce a
|
|
158
|
+
// constraint string; the only leaves we emit lines for are the ones we
|
|
159
|
+
// recognise above, so the test fixtures cover exactly the supported
|
|
160
|
+
// shapes.
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
function renderMembership(node) {
|
|
164
|
+
// call_arg[i] in [list] -> Recipient/arg must be one of [list]
|
|
165
|
+
if (node.needle.kind === 'call_arg') {
|
|
166
|
+
const list = node.haystack.map(renderHaystackElement).join(', ');
|
|
167
|
+
return `Recipient/arg must be one of [${list}]`;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
function renderVecElement(leaf) {
|
|
172
|
+
switch (leaf.kind) {
|
|
173
|
+
case 'literal_address':
|
|
174
|
+
return leaf.value;
|
|
175
|
+
case 'literal_i128':
|
|
176
|
+
return leaf.value;
|
|
177
|
+
case 'literal_symbol':
|
|
178
|
+
return leaf.value;
|
|
179
|
+
case 'literal_u32':
|
|
180
|
+
return String(leaf.value);
|
|
181
|
+
case 'literal_u64':
|
|
182
|
+
return leaf.value;
|
|
183
|
+
case 'literal_bytes':
|
|
184
|
+
return leaf.value;
|
|
185
|
+
case 'literal_vec':
|
|
186
|
+
return `[${leaf.elements.map(renderVecElement).join(', ')}]`;
|
|
187
|
+
case 'call_contract':
|
|
188
|
+
case 'call_fn':
|
|
189
|
+
case 'call_arg':
|
|
190
|
+
case 'amount':
|
|
191
|
+
case 'window_spent':
|
|
192
|
+
case 'now':
|
|
193
|
+
case 'valid_until':
|
|
194
|
+
case 'invocation_count_in_window':
|
|
195
|
+
case 'oracle_price':
|
|
196
|
+
return `<${leaf.kind}>`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function renderHaystackElement(leaf) {
|
|
200
|
+
if (leaf.kind === 'literal_address')
|
|
201
|
+
return leaf.value;
|
|
202
|
+
if (leaf.kind === 'literal_i128')
|
|
203
|
+
return leaf.value;
|
|
204
|
+
if (leaf.kind === 'literal_symbol')
|
|
205
|
+
return leaf.value;
|
|
206
|
+
if (leaf.kind === 'literal_u32')
|
|
207
|
+
return String(leaf.value);
|
|
208
|
+
if (leaf.kind === 'literal_u64')
|
|
209
|
+
return leaf.value;
|
|
210
|
+
if (leaf.kind === 'literal_bytes')
|
|
211
|
+
return leaf.value;
|
|
212
|
+
if (leaf.kind === 'literal_vec') {
|
|
213
|
+
return `[${leaf.elements.map(renderHaystackElement).join(', ')}]`;
|
|
214
|
+
}
|
|
215
|
+
return `<${leaf.kind}>`;
|
|
216
|
+
}
|
|
217
|
+
function comparisonOpText(op) {
|
|
218
|
+
switch (op) {
|
|
219
|
+
case 'lt':
|
|
220
|
+
return '<';
|
|
221
|
+
case 'lte':
|
|
222
|
+
return '<=';
|
|
223
|
+
case 'gt':
|
|
224
|
+
return '>';
|
|
225
|
+
case 'gte':
|
|
226
|
+
return '>=';
|
|
227
|
+
case 'eq':
|
|
228
|
+
return '==';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** Render the plain-English one-liner. Format: `<ruleName>: <constraints>`,
|
|
232
|
+
* joined by `; ` so the user reads one sentence per constraint. */
|
|
233
|
+
function renderPlainEnglish(ruleName, constraints) {
|
|
234
|
+
if (constraints.length === 0)
|
|
235
|
+
return `${ruleName}: (no constraints)`;
|
|
236
|
+
return `${ruleName}: ${constraints.join('; ')}`;
|
|
237
|
+
}
|
|
238
|
+
/** Render the expiry line. `null` -> "No expiry"; a ledger sequence -> the
|
|
239
|
+
* ledger number so the user reads it in the same units the OZ context rule
|
|
240
|
+
* applies it. */
|
|
241
|
+
function renderExpiry(validUntilLedger) {
|
|
242
|
+
if (validUntilLedger === null)
|
|
243
|
+
return 'No expiry';
|
|
244
|
+
return `Valid until ledger ${validUntilLedger}`;
|
|
245
|
+
}
|
|
246
|
+
function computeContentHash(input) {
|
|
247
|
+
return (0, node_crypto_1.createHash)('sha256').update(canonicalStringify(input)).digest('hex');
|
|
248
|
+
}
|
|
249
|
+
/** Canonical JSON with recursively sorted object keys (stable across runs). */
|
|
250
|
+
function canonicalStringify(value) {
|
|
251
|
+
return JSON.stringify(sortKeys(value));
|
|
252
|
+
}
|
|
253
|
+
function sortKeys(value) {
|
|
254
|
+
if (Array.isArray(value))
|
|
255
|
+
return value.map(sortKeys);
|
|
256
|
+
if (value && typeof value === 'object') {
|
|
257
|
+
const out = {};
|
|
258
|
+
for (const key of Object.keys(value).sort()) {
|
|
259
|
+
out[key] = sortKeys(value[key]);
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
return value;
|
|
264
|
+
}
|