@crediolabs/policy-synth 0.3.1 → 0.4.0
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/install/authority-overlap.d.ts +101 -0
- package/dist/install/authority-overlap.js +227 -0
- package/dist/install/index.d.ts +1 -0
- package/dist/install/index.js +4 -0
- package/dist/record/index.d.ts +10 -0
- package/dist/record/index.js +32 -1
- package/dist/record/rpc.d.ts +4 -0
- package/dist/record/rpc.js +4 -1
- package/dist/registry/identify.d.ts +10 -1
- package/dist/registry/identify.js +4 -1
- package/dist/registry/on-chain-spec.d.ts +37 -0
- package/dist/registry/on-chain-spec.js +152 -0
- package/dist/run/index.d.ts +25 -4
- package/dist/run/index.js +65 -4
- package/dist/run/schemas.d.ts +313 -0
- package/dist/run/schemas.js +52 -0
- package/dist/synth/declare.d.ts +30 -0
- package/dist/synth/declare.js +98 -0
- package/dist/synth/index.d.ts +1 -0
- package/dist/synth/index.js +1 -0
- package/dist-cjs/install/authority-overlap.d.ts +101 -0
- package/dist-cjs/install/authority-overlap.js +236 -0
- package/dist-cjs/install/index.d.ts +1 -0
- package/dist-cjs/install/index.js +13 -2
- package/dist-cjs/record/index.d.ts +10 -0
- package/dist-cjs/record/index.js +31 -0
- package/dist-cjs/record/rpc.d.ts +4 -0
- package/dist-cjs/record/rpc.js +7 -3
- package/dist-cjs/registry/identify.d.ts +10 -1
- package/dist-cjs/registry/identify.js +4 -0
- package/dist-cjs/registry/on-chain-spec.d.ts +37 -0
- package/dist-cjs/registry/on-chain-spec.js +159 -0
- package/dist-cjs/run/index.d.ts +25 -4
- package/dist-cjs/run/index.js +65 -2
- package/dist-cjs/run/schemas.d.ts +313 -0
- package/dist-cjs/run/schemas.js +53 -1
- package/dist-cjs/synth/declare.d.ts +30 -0
- package/dist-cjs/synth/declare.js +101 -0
- package/dist-cjs/synth/index.d.ts +1 -0
- package/dist-cjs/synth/index.js +3 -1
- package/package.json +1 -1
- package/src/install/authority-overlap.ts +312 -0
- package/src/install/index.ts +20 -0
- package/src/record/index.ts +59 -2
- package/src/record/rpc.ts +4 -1
- package/src/registry/identify.ts +4 -1
- package/src/registry/on-chain-spec.ts +168 -0
- package/src/run/index.ts +79 -2
- package/src/run/schemas.ts +57 -0
- package/src/synth/declare.ts +157 -0
- package/src/synth/index.ts +5 -0
package/src/run/index.ts
CHANGED
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
import { createHash } from 'node:crypto'
|
|
21
21
|
import { rpc } from '@stellar/stellar-sdk'
|
|
22
22
|
import {
|
|
23
|
+
declarePredicate,
|
|
23
24
|
type ErrorCode,
|
|
25
|
+
encodePredicate,
|
|
24
26
|
type Network,
|
|
25
27
|
type PredicateNode,
|
|
26
28
|
type ProposedPolicy,
|
|
@@ -31,6 +33,11 @@ import {
|
|
|
31
33
|
type ToolError,
|
|
32
34
|
type ToolResponse,
|
|
33
35
|
} from '../index.ts'
|
|
36
|
+
import {
|
|
37
|
+
type AuthorityOverlap,
|
|
38
|
+
findAuthorityOverlaps,
|
|
39
|
+
type ObservedRule,
|
|
40
|
+
} from '../install/authority-overlap.ts'
|
|
34
41
|
import {
|
|
35
42
|
type BuildInstallPolicyResult,
|
|
36
43
|
type BuildRevokePolicyResult,
|
|
@@ -40,8 +47,10 @@ import {
|
|
|
40
47
|
rpcClientFromServer,
|
|
41
48
|
} from '../install/build-install-policy.ts'
|
|
42
49
|
import { getInterpreterInfo } from '../install/get-interpreter-info.ts'
|
|
50
|
+
import { decodePredicate } from '../predicate/decode.ts'
|
|
43
51
|
import { type EvalContext, evaluate, generateCases } from '../simulate/index.ts'
|
|
44
52
|
import {
|
|
53
|
+
DeclarePolicyInputSchema,
|
|
45
54
|
type GetInterpreterInfoInput,
|
|
46
55
|
GetInterpreterInfoInputSchema,
|
|
47
56
|
type InstallPolicyInput,
|
|
@@ -64,6 +73,7 @@ import {
|
|
|
64
73
|
} from './schemas.ts'
|
|
65
74
|
|
|
66
75
|
export type {
|
|
76
|
+
DeclarePolicyInput,
|
|
67
77
|
GetInterpreterInfoInput,
|
|
68
78
|
InstallPolicyInput,
|
|
69
79
|
RecordTransactionInput,
|
|
@@ -78,6 +88,7 @@ export type {
|
|
|
78
88
|
// truth - MCP tool shapes are derived from them.
|
|
79
89
|
export {
|
|
80
90
|
ComposeUserResponsesSchema,
|
|
91
|
+
DeclarePolicyInputSchema,
|
|
81
92
|
GetInterpreterInfoInputSchema,
|
|
82
93
|
InstallPolicyInputSchema,
|
|
83
94
|
InterpreterOptionsSchema,
|
|
@@ -107,6 +118,7 @@ export type RunVerifyPolicyInput = VerifyPolicyInput
|
|
|
107
118
|
type RunToolName =
|
|
108
119
|
| 'record_transaction'
|
|
109
120
|
| 'synthesize_policy'
|
|
121
|
+
| 'declare_policy'
|
|
110
122
|
| 'simulate_policy'
|
|
111
123
|
| 'verify_policy'
|
|
112
124
|
| 'install_policy'
|
|
@@ -119,6 +131,7 @@ type RunToolName =
|
|
|
119
131
|
const TOOL_ERROR_CODE: Record<RunToolName, ErrorCode> = {
|
|
120
132
|
record_transaction: 'RECORDING_FAILED',
|
|
121
133
|
synthesize_policy: 'SYNTHESIS_ERROR',
|
|
134
|
+
declare_policy: 'SYNTHESIS_ERROR',
|
|
122
135
|
simulate_policy: 'SIMULATION_ERROR',
|
|
123
136
|
verify_policy: 'VERIFICATION_FAILED',
|
|
124
137
|
install_policy: 'INSTALL_BUILD_FAILED',
|
|
@@ -203,7 +216,7 @@ export async function runSynthesizePolicy(raw: unknown): Promise<
|
|
|
203
216
|
|
|
204
217
|
export async function runInstallPolicy(
|
|
205
218
|
raw: unknown
|
|
206
|
-
): Promise<ToolResponse<BuildInstallPolicyResult>> {
|
|
219
|
+
): Promise<ToolResponse<BuildInstallPolicyResult & { authorityScan: AuthorityOverlap[] | null }>> {
|
|
207
220
|
const parsed = InstallPolicyInputSchema.safeParse(raw)
|
|
208
221
|
if (!parsed.success) {
|
|
209
222
|
return { ok: false, error: validationError('install_policy', parsed.error.issues) }
|
|
@@ -254,7 +267,28 @@ export async function runInstallPolicy(
|
|
|
254
267
|
rpc: rpcClient,
|
|
255
268
|
...(input.baseFee !== undefined ? { baseFee: input.baseFee } : {}),
|
|
256
269
|
})
|
|
257
|
-
|
|
270
|
+
// Cross-rule scan, when the caller supplied what else is on the account.
|
|
271
|
+
// ABSENT is reported as `null` rather than an empty list: "we did not
|
|
272
|
+
// look" and "we looked and found nothing" are different answers, and
|
|
273
|
+
// collapsing them would let a caller read silence as safety.
|
|
274
|
+
const authorityScan =
|
|
275
|
+
input.existingRules === undefined
|
|
276
|
+
? null
|
|
277
|
+
: findAuthorityOverlaps({
|
|
278
|
+
intended: {
|
|
279
|
+
// `add_context_rule` gets its id FROM the account, so there is
|
|
280
|
+
// no existing rule this install replaces. A sentinel no real id
|
|
281
|
+
// can equal keeps every observed rule in scope.
|
|
282
|
+
ruleId: -1,
|
|
283
|
+
contextType: input.rule.contextRuleType,
|
|
284
|
+
signers: input.rule.signers,
|
|
285
|
+
predicate: decodePredicate(encodedPredicate),
|
|
286
|
+
},
|
|
287
|
+
// The schema types `predicate` loosely (it is the shared
|
|
288
|
+
// PredicateNodeSchema); the shape is already validated.
|
|
289
|
+
existing: input.existingRules as ObservedRule[],
|
|
290
|
+
})
|
|
291
|
+
return { ok: true, data: { ...result, authorityScan } }
|
|
258
292
|
} catch (e) {
|
|
259
293
|
return toolFailure('install_policy', e)
|
|
260
294
|
}
|
|
@@ -384,6 +418,49 @@ export function runSimulatePolicy(raw: unknown): ToolResponse<{
|
|
|
384
418
|
}
|
|
385
419
|
}
|
|
386
420
|
|
|
421
|
+
/** `declare_policy` body - the DECLARATIVE front-end.
|
|
422
|
+
*
|
|
423
|
+
* `synthesize_policy` infers a predicate from a transaction that happened;
|
|
424
|
+
* this takes the constraint stated outright. No RPC, no decoding and no
|
|
425
|
+
* parseConfidence, so nothing here can be refused for a contract the registry
|
|
426
|
+
* does not recognise - which is most of the point of having it.
|
|
427
|
+
*
|
|
428
|
+
* The returned `warnings` are load-bearing, not decoration. An argument index
|
|
429
|
+
* the caller did not supply is DEFAULTED to the SEP-41 position, and a bound
|
|
430
|
+
* on the wrong argument constrains something the caller did not mean without
|
|
431
|
+
* ever announcing itself, so a caller that ignores warnings can install a
|
|
432
|
+
* predicate that reads correctly and binds nothing. */
|
|
433
|
+
export function runDeclarePolicy(raw: unknown): ToolResponse<{
|
|
434
|
+
predicate: PredicateNode
|
|
435
|
+
encodedPredicate: string
|
|
436
|
+
predicateHash: string
|
|
437
|
+
warnings: string[]
|
|
438
|
+
}> {
|
|
439
|
+
const parsed = DeclarePolicyInputSchema.safeParse(raw)
|
|
440
|
+
if (!parsed.success) {
|
|
441
|
+
return { ok: false, error: validationError('declare_policy', parsed.error.issues) }
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
// Rebuilt field-by-field rather than passed through: the schema's
|
|
445
|
+
// optionals are `T | undefined` and `PolicyDeclaration`'s are absent-or-T,
|
|
446
|
+
// which `exactOptionalPropertyTypes` treats as different.
|
|
447
|
+
const d = parsed.data
|
|
448
|
+
const { predicate, warnings } = declarePredicate({
|
|
449
|
+
fn: d.fn,
|
|
450
|
+
...(d.contract !== undefined ? { contract: d.contract } : {}),
|
|
451
|
+
...(d.maxAmount !== undefined ? { maxAmount: d.maxAmount } : {}),
|
|
452
|
+
...(d.amountArgIndex !== undefined ? { amountArgIndex: d.amountArgIndex } : {}),
|
|
453
|
+
...(d.recipients !== undefined ? { recipients: d.recipients } : {}),
|
|
454
|
+
...(d.recipientArgIndex !== undefined ? { recipientArgIndex: d.recipientArgIndex } : {}),
|
|
455
|
+
...(d.allowZeroCap !== undefined ? { allowZeroCap: d.allowZeroCap } : {}),
|
|
456
|
+
})
|
|
457
|
+
const { encodedPredicate, predicateHash } = encodePredicate(predicate)
|
|
458
|
+
return { ok: true, data: { predicate, encodedPredicate, predicateHash, warnings } }
|
|
459
|
+
} catch (e) {
|
|
460
|
+
return toolFailure('declare_policy', e)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
387
464
|
/** `verify_policy` body - the permit case plus a generated deny case per
|
|
388
465
|
* dimension.
|
|
389
466
|
*
|
package/src/run/schemas.ts
CHANGED
|
@@ -281,6 +281,29 @@ export type VerifyPolicyInput = z.infer<typeof VerifyPolicyInputSchema>
|
|
|
281
281
|
const MAX_SIGNERS_PER_RULE = 15
|
|
282
282
|
const MAX_POLICIES_PER_RULE = 5
|
|
283
283
|
|
|
284
|
+
const SignerDraftSchema = z.discriminatedUnion('kind', [
|
|
285
|
+
z.object({ kind: z.literal('delegated'), address: z.string() }),
|
|
286
|
+
z.object({ kind: z.literal('external'), verifier: z.string(), keyBytes: z.string() }),
|
|
287
|
+
])
|
|
288
|
+
|
|
289
|
+
const ContextTypeSchema = z.discriminatedUnion('kind', [
|
|
290
|
+
z.object({ kind: z.literal('default') }),
|
|
291
|
+
z.object({ kind: z.literal('call_contract'), contract: z.string() }),
|
|
292
|
+
z.object({ kind: z.literal('create_contract'), wasmHash: z.string() }),
|
|
293
|
+
])
|
|
294
|
+
|
|
295
|
+
/** A rule ALREADY on the account, as the caller observed it. Supplying these
|
|
296
|
+
* turns on the cross-rule authority scan: a signer belonging to several rules
|
|
297
|
+
* picks which one applies, so a predicate only constrains a key when the
|
|
298
|
+
* policed rule is the only rule that key is on. */
|
|
299
|
+
export const ObservedRuleSchema = z.object({
|
|
300
|
+
id: z.number().int().nonnegative(),
|
|
301
|
+
contextType: ContextTypeSchema,
|
|
302
|
+
signers: z.array(SignerDraftSchema),
|
|
303
|
+
policyAddresses: z.array(z.string()),
|
|
304
|
+
predicate: PredicateNodeSchema.optional(),
|
|
305
|
+
})
|
|
306
|
+
|
|
284
307
|
const ContextRuleDraftSchema = z
|
|
285
308
|
.object({
|
|
286
309
|
contextRuleType: z.discriminatedUnion('kind', [
|
|
@@ -391,8 +414,42 @@ export const NETWORK_PASSPHRASES: Record<Network, string> = {
|
|
|
391
414
|
const STELLAR_CONTRACT_ADDRESS = /^C[2-7A-Z]{55}$/
|
|
392
415
|
const STELLAR_ACCOUNT_ADDRESS = /^G[2-7A-Z]{55}$/
|
|
393
416
|
|
|
417
|
+
// ===== declare_policy =====
|
|
418
|
+
//
|
|
419
|
+
// The declarative front-end: the constraint stated outright, with no
|
|
420
|
+
// transaction to decode. Deliberately NOT a revival of the removed
|
|
421
|
+
// `MandateSpec` - that carried a rolling `spendingLimit` the interpreter
|
|
422
|
+
// cannot evaluate and an `approvalThreshold` needing OZ primitives nobody
|
|
423
|
+
// deployed. Only fields grammar 3 can actually enforce appear here.
|
|
424
|
+
export const DeclarePolicyInputSchema = z
|
|
425
|
+
.object({
|
|
426
|
+
fn: z.string().min(1, 'fn must name the method to pin'),
|
|
427
|
+
contract: z
|
|
428
|
+
.string()
|
|
429
|
+
.regex(STELLAR_CONTRACT_ADDRESS, 'contract must be a Stellar contract address (C...)')
|
|
430
|
+
.optional(),
|
|
431
|
+
/** Smallest unit, unsigned decimal STRING - an i128 is wider than
|
|
432
|
+
* Number.MAX_SAFE_INTEGER, so a number here would silently round. */
|
|
433
|
+
maxAmount: z
|
|
434
|
+
.string()
|
|
435
|
+
.regex(/^[0-9]+$/, 'maxAmount must be an unsigned integer in the smallest unit')
|
|
436
|
+
.optional(),
|
|
437
|
+
amountArgIndex: z.number().int().nonnegative().max(U32_MAX).optional(),
|
|
438
|
+
recipients: z.array(z.string()).min(1, 'recipients must not be empty').optional(),
|
|
439
|
+
recipientArgIndex: z.number().int().nonnegative().max(U32_MAX).optional(),
|
|
440
|
+
allowZeroCap: z.boolean().optional(),
|
|
441
|
+
})
|
|
442
|
+
.strict()
|
|
443
|
+
export type DeclarePolicyInput = z.infer<typeof DeclarePolicyInputSchema>
|
|
444
|
+
|
|
394
445
|
export const InstallPolicyInputSchema = z
|
|
395
446
|
.object({
|
|
447
|
+
/** Rules already on the account. Supplying them turns on the cross-rule
|
|
448
|
+
* authority scan, which reports every existing rule a signer of this
|
|
449
|
+
* install could name INSTEAD - including an unpoliced one, against which
|
|
450
|
+
* the predicate never runs. Absent means the scan is skipped, and the
|
|
451
|
+
* result says so rather than reporting "no overlaps found". */
|
|
452
|
+
existingRules: z.array(ObservedRuleSchema).optional(),
|
|
396
453
|
/** The smart account contract address (C...) that will receive the rule. */
|
|
397
454
|
smartAccount: z
|
|
398
455
|
.string()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// src/synth/declare.ts - build a predicate from a DECLARED constraint.
|
|
2
|
+
//
|
|
3
|
+
// The second synthesis front-end, beside `synthesizeFromRecording`. That one
|
|
4
|
+
// infers a predicate from a transaction that happened; this one takes the
|
|
5
|
+
// constraint the user states outright. No transaction, no decoding, no
|
|
6
|
+
// inference, no RPC: the same declaration always lowers to a byte-identical
|
|
7
|
+
// predicate.
|
|
8
|
+
//
|
|
9
|
+
// This is the surviving half of the removed `MandateSpec`. Two of that type's
|
|
10
|
+
// five fields lowered to things that do not exist - `spendingLimit` became a
|
|
11
|
+
// `window_spent` compare the interpreter cannot evaluate (it is handed one
|
|
12
|
+
// call and keeps no state), and `approvalThreshold` needed OZ built-in policy
|
|
13
|
+
// contracts that were never deployed. Both are deliberately absent here, and
|
|
14
|
+
// the per-call `maxAmount` bound below is the honest replacement for the
|
|
15
|
+
// first: it constrains the amount in THIS call rather than implying a rolling
|
|
16
|
+
// total nothing tracks.
|
|
17
|
+
//
|
|
18
|
+
// What a declaration can say maps one-to-one onto grammar 3:
|
|
19
|
+
// fn -> eq(call_fn, literal_symbol)
|
|
20
|
+
// contract -> eq(call_contract, literal_address)
|
|
21
|
+
// maxAmount -> lte(call_arg(i), literal_i128)
|
|
22
|
+
// recipients -> in(call_arg(j), [literal_address, ...])
|
|
23
|
+
|
|
24
|
+
import type { ToolError } from '../errors.ts'
|
|
25
|
+
import type { PredicateLeaf, PredicateNode } from '../types.ts'
|
|
26
|
+
import { isStellarAddress } from './address.ts'
|
|
27
|
+
|
|
28
|
+
/** Argument positions of the SEP-41 `transfer(from, to, amount)` shape. A
|
|
29
|
+
* declaration that names a different method almost certainly has different
|
|
30
|
+
* positions, which is why using either default emits a warning naming the
|
|
31
|
+
* index it assumed - a bound on the wrong argument constrains something the
|
|
32
|
+
* user did not mean and fails silently. */
|
|
33
|
+
const SEP41_RECIPIENT_ARG = 1
|
|
34
|
+
const SEP41_AMOUNT_ARG = 2
|
|
35
|
+
|
|
36
|
+
export interface PolicyDeclaration {
|
|
37
|
+
/** Method to pin. Required: a predicate with no selector leaf constrains
|
|
38
|
+
* nothing and the contract refuses it at install. */
|
|
39
|
+
fn: string
|
|
40
|
+
/** Contract to pin, already resolved to a `C...` address. */
|
|
41
|
+
contract?: string
|
|
42
|
+
/** Upper bound on the call's amount argument, in the token's SMALLEST
|
|
43
|
+
* unit as an unsigned decimal string (25 XLM = "250000000"). */
|
|
44
|
+
maxAmount?: string
|
|
45
|
+
/** Which argument carries the amount. Defaults to the SEP-41 position. */
|
|
46
|
+
amountArgIndex?: number
|
|
47
|
+
/** Recipient allowlist. */
|
|
48
|
+
recipients?: string[]
|
|
49
|
+
/** Which argument carries the recipient. Defaults to the SEP-41 position. */
|
|
50
|
+
recipientArgIndex?: number
|
|
51
|
+
/** A cap of "0" denies every call, so it is refused unless asked for
|
|
52
|
+
* explicitly. A rule that permits nothing is a plausible thing to want and
|
|
53
|
+
* an implausible thing to want by accident. */
|
|
54
|
+
allowZeroCap?: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DeclaredPredicate {
|
|
58
|
+
predicate: PredicateNode
|
|
59
|
+
/** Assumptions the caller should check. Never empty when an argument index
|
|
60
|
+
* was defaulted rather than supplied. */
|
|
61
|
+
warnings: string[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Lower a declared constraint to a grammar-3 predicate. Pure and total:
|
|
65
|
+
* the same declaration always produces the same predicate. */
|
|
66
|
+
export function declarePredicate(d: PolicyDeclaration): DeclaredPredicate {
|
|
67
|
+
if (!d.fn || d.fn.trim() === '') {
|
|
68
|
+
throw declareError('SYNTHESIS_ERROR', 'a declaration needs `fn`: the method to pin')
|
|
69
|
+
}
|
|
70
|
+
const warnings: string[] = []
|
|
71
|
+
const children: PredicateNode[] = [
|
|
72
|
+
{ op: 'eq', left: { kind: 'call_fn' }, right: { kind: 'literal_symbol', value: d.fn } },
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
if (d.contract !== undefined) {
|
|
76
|
+
if (!isStellarAddress(d.contract) || !d.contract.startsWith('C')) {
|
|
77
|
+
throw declareError(
|
|
78
|
+
'SYNTHESIS_ERROR',
|
|
79
|
+
`contract must be a Stellar contract address (C...), got ${d.contract}`
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
children.push({
|
|
83
|
+
op: 'eq',
|
|
84
|
+
left: { kind: 'call_contract' },
|
|
85
|
+
right: { kind: 'literal_address', value: d.contract },
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (d.recipients !== undefined) {
|
|
90
|
+
if (d.recipients.length === 0) {
|
|
91
|
+
throw declareError(
|
|
92
|
+
'SYNTHESIS_ERROR',
|
|
93
|
+
'recipients was supplied but empty; an empty `in` haystack is refused at decode. Omit it to leave recipients unconstrained.'
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
for (const r of d.recipients) {
|
|
97
|
+
if (!isStellarAddress(r)) {
|
|
98
|
+
throw declareError('SYNTHESIS_ERROR', `recipient is not a Stellar address: ${r}`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const idx = d.recipientArgIndex ?? SEP41_RECIPIENT_ARG
|
|
102
|
+
if (d.recipientArgIndex === undefined) {
|
|
103
|
+
warnings.push(
|
|
104
|
+
`recipient allowlist bound to call_arg(${idx}), the SEP-41 \`transfer\` position. If \`${d.fn}\` carries the recipient elsewhere this constrains the wrong argument - pass recipientArgIndex.`
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
children.push({
|
|
108
|
+
op: 'in',
|
|
109
|
+
needle: { kind: 'call_arg', index: idx },
|
|
110
|
+
haystack: d.recipients.map((value): PredicateLeaf => ({ kind: 'literal_address', value })),
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (d.maxAmount !== undefined) {
|
|
115
|
+
if (!/^[0-9]+$/.test(d.maxAmount)) {
|
|
116
|
+
throw declareError(
|
|
117
|
+
'SYNTHESIS_ERROR',
|
|
118
|
+
`maxAmount must be an unsigned integer in the token's smallest unit, got "${d.maxAmount}" (25 XLM = "250000000")`
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
if (d.maxAmount === '0' && d.allowZeroCap !== true) {
|
|
122
|
+
throw declareError(
|
|
123
|
+
'SYNTHESIS_ERROR',
|
|
124
|
+
'maxAmount "0" denies every call: no amount satisfies the bound. Set allowZeroCap to declare that deliberately.'
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
const idx = d.amountArgIndex ?? SEP41_AMOUNT_ARG
|
|
128
|
+
if (d.amountArgIndex === undefined) {
|
|
129
|
+
warnings.push(
|
|
130
|
+
`amount cap bound to call_arg(${idx}), the SEP-41 \`transfer\` position. If \`${d.fn}\` carries the amount elsewhere this caps the wrong argument - pass amountArgIndex.`
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
children.push({
|
|
134
|
+
op: 'lte',
|
|
135
|
+
left: { kind: 'call_arg', index: idx },
|
|
136
|
+
right: { kind: 'literal_i128', value: d.maxAmount },
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// A single conjunct is emitted bare. `and` with one child encodes to
|
|
141
|
+
// different bytes than the child alone, and the extra node buys nothing.
|
|
142
|
+
const predicate: PredicateNode =
|
|
143
|
+
children.length === 1 ? (children[0] as PredicateNode) : { op: 'and', children }
|
|
144
|
+
return { predicate, warnings }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function declareError(code: ToolError['code'], message: string): ToolError {
|
|
148
|
+
const err = new Error(message) as Error & {
|
|
149
|
+
code: ToolError['code']
|
|
150
|
+
severity: string
|
|
151
|
+
retryable: boolean
|
|
152
|
+
}
|
|
153
|
+
err.code = code
|
|
154
|
+
err.severity = 'error'
|
|
155
|
+
err.retryable = false
|
|
156
|
+
throw err
|
|
157
|
+
}
|
package/src/synth/index.ts
CHANGED
|
@@ -9,6 +9,11 @@ export {
|
|
|
9
9
|
type ComposeResult,
|
|
10
10
|
composeFromRecording,
|
|
11
11
|
} from './compose-from-recording.ts'
|
|
12
|
+
export {
|
|
13
|
+
type DeclaredPredicate,
|
|
14
|
+
declarePredicate,
|
|
15
|
+
type PolicyDeclaration,
|
|
16
|
+
} from './declare.ts'
|
|
12
17
|
export { type IntentFacts, lower } from './lower.ts'
|
|
13
18
|
export {
|
|
14
19
|
type DecideScopeOptions,
|