@crediolabs/policy-synth 0.1.17 → 0.2.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/README.md +44 -246
- package/dist/adapters/interpreter/adapter.d.ts +6 -7
- package/dist/adapters/interpreter/adapter.js +28 -48
- package/dist/adapters/oz/adapter.js +12 -13
- package/dist/codegen/compile-gate.js +5 -0
- package/dist/codegen/template.js +17 -0
- package/dist/install/authority-overlap.d.ts +134 -0
- package/dist/install/authority-overlap.js +0 -0
- package/dist/install/build-add-context-rule.d.ts +8 -0
- package/dist/install/build-add-context-rule.js +17 -46
- package/dist/install/build-install-policy.d.ts +12 -5
- package/dist/install/build-install-policy.js +32 -23
- package/dist/install/build-merge-policy.d.ts +70 -0
- package/dist/install/build-merge-policy.js +130 -0
- package/dist/install/get-interpreter-info.js +2 -2
- package/dist/install/index.d.ts +3 -1
- package/dist/install/index.js +8 -1
- package/dist/install/oz-auth.js +5 -3
- package/dist/install/plan-merge-policy.d.ts +49 -0
- package/dist/install/plan-merge-policy.js +86 -0
- package/dist/install/read-account-rules.d.ts +100 -0
- package/dist/install/read-account-rules.js +283 -0
- package/dist/predicate/decode.js +1 -1
- package/dist/predicate/encode.js +171 -10
- package/dist/record/decode.js +5 -6
- package/dist/registry/protocols.d.ts +1 -1
- package/dist/registry/protocols.js +1 -1
- package/dist/review-card/builder.d.ts +6 -0
- package/dist/review-card/builder.js +9 -1
- package/dist/run/index.d.ts +104 -18
- package/dist/run/index.js +359 -75
- package/dist/run/schemas.d.ts +479 -18
- package/dist/run/schemas.js +132 -24
- package/dist/synth/compose-from-recording.d.ts +10 -15
- package/dist/synth/compose-from-recording.js +79 -124
- package/dist/synth/deny-cases.d.ts +5 -0
- package/dist/synth/deny-cases.js +52 -13
- package/dist/synth/evaluate.js +69 -119
- package/dist/synth/harness.d.ts +13 -1
- package/dist/synth/harness.js +22 -1
- package/dist/synth/index.d.ts +1 -1
- package/dist/synth/synthesize-from-recording.d.ts +31 -21
- package/dist/synth/synthesize-from-recording.js +129 -155
- package/dist-cjs/adapters/interpreter/adapter.d.ts +6 -7
- package/dist-cjs/adapters/interpreter/adapter.js +28 -48
- package/dist-cjs/adapters/oz/adapter.js +12 -13
- package/dist-cjs/codegen/compile-gate.js +5 -0
- package/dist-cjs/codegen/template.js +17 -0
- package/dist-cjs/install/authority-overlap.d.ts +134 -0
- package/dist-cjs/install/authority-overlap.js +0 -0
- package/dist-cjs/install/build-add-context-rule.d.ts +8 -0
- package/dist-cjs/install/build-add-context-rule.js +17 -45
- package/dist-cjs/install/build-install-policy.d.ts +12 -5
- package/dist-cjs/install/build-install-policy.js +32 -23
- package/dist-cjs/install/build-merge-policy.d.ts +70 -0
- package/dist-cjs/install/build-merge-policy.js +134 -0
- package/dist-cjs/install/get-interpreter-info.js +2 -2
- package/dist-cjs/install/index.d.ts +3 -1
- package/dist-cjs/install/index.js +24 -3
- package/dist-cjs/install/oz-auth.js +5 -3
- package/dist-cjs/install/plan-merge-policy.d.ts +49 -0
- package/dist-cjs/install/plan-merge-policy.js +90 -0
- package/dist-cjs/install/read-account-rules.d.ts +100 -0
- package/dist-cjs/install/read-account-rules.js +296 -0
- package/dist-cjs/predicate/decode.js +1 -1
- package/dist-cjs/predicate/encode.js +171 -10
- package/dist-cjs/record/decode.js +5 -6
- package/dist-cjs/registry/protocols.d.ts +1 -1
- package/dist-cjs/registry/protocols.js +1 -1
- package/dist-cjs/review-card/builder.d.ts +6 -0
- package/dist-cjs/review-card/builder.js +9 -1
- package/dist-cjs/run/index.d.ts +104 -18
- package/dist-cjs/run/index.js +360 -74
- package/dist-cjs/run/schemas.d.ts +479 -18
- package/dist-cjs/run/schemas.js +133 -25
- package/dist-cjs/synth/compose-from-recording.d.ts +10 -15
- package/dist-cjs/synth/compose-from-recording.js +79 -124
- package/dist-cjs/synth/deny-cases.d.ts +5 -0
- package/dist-cjs/synth/deny-cases.js +52 -13
- package/dist-cjs/synth/evaluate.js +69 -119
- package/dist-cjs/synth/harness.d.ts +13 -1
- package/dist-cjs/synth/harness.js +22 -1
- package/dist-cjs/synth/index.d.ts +1 -1
- package/dist-cjs/synth/synthesize-from-recording.d.ts +31 -21
- package/dist-cjs/synth/synthesize-from-recording.js +129 -153
- package/package.json +1 -1
- package/src/adapters/interpreter/adapter.ts +28 -48
- package/src/adapters/oz/adapter.ts +12 -13
- package/src/codegen/compile-gate.ts +5 -0
- package/src/codegen/template.ts +17 -0
- package/src/install/authority-overlap.ts +0 -0
- package/src/install/build-add-context-rule.ts +29 -56
- package/src/install/build-install-policy.ts +41 -23
- package/src/install/build-merge-policy.ts +219 -0
- package/src/install/get-interpreter-info.ts +2 -2
- package/src/install/index.ts +36 -2
- package/src/install/oz-auth.ts +5 -3
- package/src/install/plan-merge-policy.ts +133 -0
- package/src/install/read-account-rules.ts +376 -0
- package/src/predicate/decode.ts +1 -1
- package/src/predicate/encode.ts +176 -10
- package/src/record/decode.ts +5 -6
- package/src/registry/protocols.ts +1 -1
- package/src/review-card/builder.ts +17 -1
- package/src/run/index.ts +500 -102
- package/src/run/schemas.ts +145 -24
- package/src/synth/compose-from-recording.ts +87 -132
- package/src/synth/deny-cases.ts +59 -13
- package/src/synth/evaluate.ts +70 -118
- package/src/synth/harness.ts +29 -1
- package/src/synth/index.ts +1 -0
- package/src/synth/synthesize-from-recording.ts +209 -192
- package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +0 -196
package/src/run/schemas.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// step with the TS source of truth.
|
|
8
8
|
//
|
|
9
9
|
// i128 amounts and other large integers are carried as base-10 decimal strings
|
|
10
|
-
// end-to-end
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
// end-to-end. Networks are pinned to the same closed set the core defines.
|
|
11
|
+
// The discriminated union on `source` exposes BOTH synthesize_policy
|
|
12
|
+
// front-ends through a single tool input.
|
|
13
13
|
//
|
|
14
14
|
// This module is the SINGLE source of truth for these shapes. The MCP package
|
|
15
15
|
// imports them here so its tool-shape bindings stay in step; the CLI imports
|
|
@@ -139,6 +139,25 @@ export const MandateSpecSchema = z
|
|
|
139
139
|
.optional(),
|
|
140
140
|
})
|
|
141
141
|
.passthrough()
|
|
142
|
+
// TS-F4/F6: a `recipients` allowlist is only meaningful against a SEP-41
|
|
143
|
+
// method whose arg 1 IS the recipient (SAC/SEP-41 `transfer(from, to,
|
|
144
|
+
// amount)` and SEP-41 `mint(to, amount)`). Without this gate the
|
|
145
|
+
// `to-ir.ts` lowering pins the allowlist to `RECIPIENT_ARG_INDEX = 1`
|
|
146
|
+
// for any contract+method, which would let a non-SEP-41 method's
|
|
147
|
+
// arg[1] (e.g. an amount, an op type, an arbitrary address payload)
|
|
148
|
+
// be silently constrained as if it were a recipient. Refusing
|
|
149
|
+
// non-SEP-41 methods at the boundary is the fail-closed shape.
|
|
150
|
+
.refine(
|
|
151
|
+
(v) =>
|
|
152
|
+
v.recipients === undefined ||
|
|
153
|
+
v.recipients.length === 0 ||
|
|
154
|
+
v.method === 'transfer' ||
|
|
155
|
+
v.method === 'mint',
|
|
156
|
+
{
|
|
157
|
+
message:
|
|
158
|
+
'recipients is only valid when method is a SEP-41 method (transfer or mint); other methods do not have a recipient at arg[1]',
|
|
159
|
+
}
|
|
160
|
+
)
|
|
142
161
|
|
|
143
162
|
/** ComposeUserResponses mirrors the core. */
|
|
144
163
|
export const ComposeUserResponsesSchema = z
|
|
@@ -157,6 +176,23 @@ export const ComposeUserResponsesSchema = z
|
|
|
157
176
|
swapRecipientAllowlist: z
|
|
158
177
|
.array(z.string().refine(isStellarAddress, 'must be a Stellar address (G... or C...)'))
|
|
159
178
|
.optional(),
|
|
179
|
+
// Per-asset oracle-price bound, one entry per asset. Reaches
|
|
180
|
+
// `compose-from-recording` and emits an `oracle_price` compare. Declared
|
|
181
|
+
// here because the object is `.passthrough()`: the field already worked
|
|
182
|
+
// undeclared, which left the accepted surface wider than the documented
|
|
183
|
+
// one. `decimals` is REQUIRED - oracle prices normalise to 9 dp and a
|
|
184
|
+
// threshold silently assumed to share that basis is what let a raw 14-dp
|
|
185
|
+
// bound permit everything.
|
|
186
|
+
oraclePriceBound: z
|
|
187
|
+
.array(
|
|
188
|
+
z.object({
|
|
189
|
+
asset: z.string().refine(isStellarAddress, 'must be a Stellar address (G... or C...)'),
|
|
190
|
+
operator: z.enum(['eq', 'lt', 'lte', 'gt', 'gte']),
|
|
191
|
+
value: z.string().regex(/^[0-9]+$/),
|
|
192
|
+
decimals: z.number().int().min(0).max(U32_MAX),
|
|
193
|
+
})
|
|
194
|
+
)
|
|
195
|
+
.optional(),
|
|
160
196
|
})
|
|
161
197
|
.passthrough()
|
|
162
198
|
|
|
@@ -293,7 +329,12 @@ export const PredicateLeafSchema: z.ZodType<unknown> = z.lazy(() =>
|
|
|
293
329
|
windowSeconds: z.number().int().positive(),
|
|
294
330
|
}),
|
|
295
331
|
z.object({ kind: z.literal('now') }),
|
|
296
|
-
|
|
332
|
+
// `valid_until` is NOT in the public predicate grammar: the encoder +
|
|
333
|
+
// decoder already throw on it, so accepting it here would let a
|
|
334
|
+
// hand-crafted payload through the schema and only surface a 200+
|
|
335
|
+
// character internal-commentary error at encode time. Drop it - the
|
|
336
|
+
// policy's expiry is carried at the install layer (MandateSpec +
|
|
337
|
+
// validUntilLedger) instead.
|
|
297
338
|
z.object({
|
|
298
339
|
kind: z.literal('invocation_count_in_window'),
|
|
299
340
|
windowSecs: z.number().int().positive(),
|
|
@@ -475,26 +516,36 @@ const MandateSpecSchemaForRule = z
|
|
|
475
516
|
// reference resolves at module init. They are the new tools on top of the
|
|
476
517
|
// existing four.
|
|
477
518
|
|
|
478
|
-
/** Pinned interpreter address (testnet).
|
|
519
|
+
/** Pinned interpreter address (testnet).
|
|
479
520
|
* Single source for the MCP layer; do not embed elsewhere. */
|
|
480
521
|
export const PINNED_INTERPRETER_TESTNET_ADDRESS =
|
|
481
|
-
'
|
|
482
|
-
|
|
483
|
-
/** Pinned interpreter address (mainnet)
|
|
484
|
-
*
|
|
485
|
-
* that was exercised on testnet (same wasm sha256, see
|
|
486
|
-
* PINNED_INTERPRETER_WASM_SHA256). The address differs because instance
|
|
487
|
-
* ids are network-scoped. UNAUDITED at the time of writing - see
|
|
488
|
-
* DEPLOYMENTS.md:29-32 for the recorded decision and what is still pending. */
|
|
522
|
+
'CALHNU4LXZRKFAXYRBODTGDANTNXHJPBTJNYGLUWLIFIP24NWSGWIE4K'
|
|
523
|
+
|
|
524
|
+
/** Pinned interpreter address (mainnet), deployed 2026-08-04.
|
|
525
|
+
* UNAUDITED at the time of writing. */
|
|
489
526
|
export const PINNED_INTERPRETER_MAINNET_ADDRESS =
|
|
490
527
|
'CALZAMUPREIRY4TULBEXIK77AUTOEJG63XLCPUWEHHQDOVK6ZVVS7VQ2'
|
|
491
528
|
|
|
492
|
-
/** Pinned interpreter wasm sha256 (hex)
|
|
529
|
+
/** Pinned interpreter wasm sha256 (hex), mainnet.
|
|
530
|
+
*
|
|
531
|
+
* The two networks no longer run the same binary. Testnet carries the
|
|
532
|
+
* selector-leaf minimum and the signer-set cap; mainnet predates both. Read
|
|
533
|
+
* the hash through `PINNED_INTERPRETER_WASM_SHA256_BY_NETWORK` rather than
|
|
534
|
+
* this constant unless mainnet is specifically what is meant, or
|
|
535
|
+
* `get_interpreter_info` will report a hash the queried network does not
|
|
536
|
+
* run. */
|
|
493
537
|
export const PINNED_INTERPRETER_WASM_SHA256 =
|
|
494
538
|
'6e6c13d93e197aa380303a42cd120f5ddb080dd36ef2a343ee1dbd04ca52a443'
|
|
495
539
|
|
|
540
|
+
/** Pinned interpreter wasm sha256 (hex), testnet. Byte-identical to the
|
|
541
|
+
* artifact built from `contracts/policy-interpreter` at the commit that
|
|
542
|
+
* introduced errors 216 and 217, verified by fetching the deployed wasm back
|
|
543
|
+
* off chain. */
|
|
544
|
+
export const PINNED_INTERPRETER_WASM_SHA256_TESTNET =
|
|
545
|
+
'a4d58bc88fd82bbb8e223941ba5889db919717fae92ced13f6f55bfe583b8e22'
|
|
546
|
+
|
|
496
547
|
/** The grammar version the interpreter enforces (matches SELF_VERSION in
|
|
497
|
-
*
|
|
548
|
+
* contracts/policy-interpreter/src/version.rs). */
|
|
498
549
|
export const PINNED_INTERPRETER_GRAMMAR_VERSION = 1
|
|
499
550
|
|
|
500
551
|
/** Default Soroban RPC for the install / revoke / info tools. The recorder
|
|
@@ -509,14 +560,18 @@ export const TESTNET_RPC_URL = 'https://soroban-testnet.stellar.org'
|
|
|
509
560
|
* the deploy script hit during the 2026-08-04 mainnet rollout. */
|
|
510
561
|
export const MAINNET_RPC_URL = 'https://mainnet.sorobanrpc.com'
|
|
511
562
|
|
|
512
|
-
/** Pin + RPC lookup for the gate enforcement.
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
563
|
+
/** Pin + RPC lookup for the gate enforcement. Addresses, RPCs and wasm
|
|
564
|
+
* hashes are all network-scoped: the networks diverged when the
|
|
565
|
+
* selector-leaf and signer-cap controls were deployed to testnet ahead of
|
|
566
|
+
* mainnet. */
|
|
516
567
|
export const PINNED_INTERPRETER_ADDRESS_BY_NETWORK: Record<Network, string> = {
|
|
517
568
|
testnet: PINNED_INTERPRETER_TESTNET_ADDRESS,
|
|
518
569
|
mainnet: PINNED_INTERPRETER_MAINNET_ADDRESS,
|
|
519
570
|
}
|
|
571
|
+
export const PINNED_INTERPRETER_WASM_SHA256_BY_NETWORK: Record<Network, string> = {
|
|
572
|
+
testnet: PINNED_INTERPRETER_WASM_SHA256_TESTNET,
|
|
573
|
+
mainnet: PINNED_INTERPRETER_WASM_SHA256,
|
|
574
|
+
}
|
|
520
575
|
export const RPC_URL_BY_NETWORK: Record<Network, string> = {
|
|
521
576
|
testnet: TESTNET_RPC_URL,
|
|
522
577
|
mainnet: MAINNET_RPC_URL,
|
|
@@ -534,13 +589,27 @@ export const NETWORK_PASSPHRASES: Record<Network, string> = {
|
|
|
534
589
|
mainnet: 'Public Global Stellar Network ; September 2015',
|
|
535
590
|
}
|
|
536
591
|
|
|
592
|
+
// Stellar strkey shapes (StrKey base32, no checksum byte, version byte
|
|
593
|
+
// 0x30/0x40 prefixed by the decoder, then 32 base32 chars giving a 56-char
|
|
594
|
+
// total). The shared `isStellarAddress` helper accepts BOTH wallet (G...) and
|
|
595
|
+
// contract (C...) addresses; the install schema has to distinguish them
|
|
596
|
+
// because the smart account is a contract (C...) - a wallet address would
|
|
597
|
+
// mean the rule is being installed against the WRONG account kind entirely.
|
|
598
|
+
// `sourceAccount` is the signing wallet (G...).
|
|
599
|
+
const STELLAR_CONTRACT_ADDRESS = /^C[2-7A-Z]{55}$/
|
|
600
|
+
const STELLAR_ACCOUNT_ADDRESS = /^G[2-7A-Z]{55}$/
|
|
601
|
+
|
|
537
602
|
export const InstallPolicyInputSchema = z
|
|
538
603
|
.object({
|
|
539
604
|
/** The smart account contract address (C...) that will receive the rule. */
|
|
540
|
-
smartAccount: z
|
|
605
|
+
smartAccount: z
|
|
606
|
+
.string()
|
|
607
|
+
.regex(STELLAR_CONTRACT_ADDRESS, 'smartAccount must be a Stellar contract address (C...)'),
|
|
541
608
|
/** The signer that authorises the install (G... wallet). Used only for
|
|
542
609
|
* sequence number + auth nonce simulation; never persisted, never signed. */
|
|
543
|
-
sourceAccount: z
|
|
610
|
+
sourceAccount: z
|
|
611
|
+
.string()
|
|
612
|
+
.regex(STELLAR_ACCOUNT_ADDRESS, 'sourceAccount must be a Stellar account address (G...)'),
|
|
544
613
|
/** Target network for the install. Selects which interpreter pin and
|
|
545
614
|
* which RPC URL are valid by default. Defaults to `testnet` so the
|
|
546
615
|
* pre-mainnet callers keep working: they were always pointing at
|
|
@@ -570,6 +639,17 @@ export const InstallPolicyInputSchema = z
|
|
|
570
639
|
* everything. Selecting `network: 'mainnet'` is NOT an opt-in -
|
|
571
640
|
* the mainnet pin is its own deny-by-default anchor. */
|
|
572
641
|
allowUnpinnedInterpreter: z.boolean().optional(),
|
|
642
|
+
/** Opt-in to installing when a signer of this rule can already reach the
|
|
643
|
+
* same calls through a context rule that has NO policy attached. OZ lets
|
|
644
|
+
* the signer choose which rule authorises a call and enforces only that
|
|
645
|
+
* rule's policies, so an unpoliced rule covering the same calls makes
|
|
646
|
+
* this policy decorative. Default-deny, because the caller almost
|
|
647
|
+
* certainly believes they are restricting something. */
|
|
648
|
+
allowAuthorityOverlap: z.boolean().optional(),
|
|
649
|
+
/** Skip the cross-rule authority scan entirely. The scan costs one RPC
|
|
650
|
+
* read per rule on the account; skipping it means the response carries
|
|
651
|
+
* no statement about what the signers can already do. */
|
|
652
|
+
skipAuthorityScan: z.boolean().optional(),
|
|
573
653
|
/** Base fee in stroops; defaults to BASE_FEE (100). */
|
|
574
654
|
baseFee: z.number().int().positive().optional(),
|
|
575
655
|
})
|
|
@@ -578,15 +658,49 @@ export const InstallPolicyInputSchema = z
|
|
|
578
658
|
})
|
|
579
659
|
export type InstallPolicyInput = z.infer<typeof InstallPolicyInputSchema>
|
|
580
660
|
|
|
661
|
+
/** `merge_policy` input.
|
|
662
|
+
*
|
|
663
|
+
* The tightening remedy for a cross-rule overlap: replace a rule's predicate
|
|
664
|
+
* with the conjunction of it and a new one. Two transactions, in order,
|
|
665
|
+
* because OZ refuses to re-attach a policy already on the rule - so the
|
|
666
|
+
* caller runs `detach`, waits for it to confirm, then runs `reinstall`. */
|
|
667
|
+
export const MergePolicyInputSchema = z
|
|
668
|
+
.object({
|
|
669
|
+
smartAccount: z
|
|
670
|
+
.string()
|
|
671
|
+
.regex(STELLAR_CONTRACT_ADDRESS, 'smartAccount must be a Stellar contract address (C...)'),
|
|
672
|
+
sourceAccount: z
|
|
673
|
+
.string()
|
|
674
|
+
.regex(STELLAR_ACCOUNT_ADDRESS, 'sourceAccount must be a Stellar account address (G...)'),
|
|
675
|
+
/** The rule whose predicate is being tightened. */
|
|
676
|
+
ruleId: z.number().int().nonnegative(),
|
|
677
|
+
/** The predicate to conjoin, base64 canonical ScVal, as emitted by
|
|
678
|
+
* `synthesize_policy`. */
|
|
679
|
+
incomingPredicateBlobBase64: z.string().min(1),
|
|
680
|
+
/** Which half of the remedy to build. */
|
|
681
|
+
step: z.enum(['detach', 'reinstall']),
|
|
682
|
+
network: NetworkSchema.optional(),
|
|
683
|
+
rpcUrl: z.string().url().optional(),
|
|
684
|
+
allowUnpinnedRpcUrl: z.boolean().optional(),
|
|
685
|
+
baseFee: z.number().int().positive().optional(),
|
|
686
|
+
})
|
|
687
|
+
.strict()
|
|
688
|
+
|
|
689
|
+
export type MergePolicyInput = z.infer<typeof MergePolicyInputSchema>
|
|
690
|
+
|
|
581
691
|
export const RevokePolicyInputSchema = z
|
|
582
692
|
.object({
|
|
583
693
|
/** The smart account contract address (C...). */
|
|
584
|
-
smartAccount: z
|
|
694
|
+
smartAccount: z
|
|
695
|
+
.string()
|
|
696
|
+
.regex(STELLAR_CONTRACT_ADDRESS, 'smartAccount must be a Stellar contract address (C...)'),
|
|
585
697
|
/** The wallet that will sign the removal. The ACCOUNT decides whether it
|
|
586
698
|
* accepts that signer; this schema does not assert a rule it cannot
|
|
587
699
|
* verify, since the account's source is not in this repo. Proven on
|
|
588
700
|
* testnet: the account's deployer can revoke. */
|
|
589
|
-
sourceAccount: z
|
|
701
|
+
sourceAccount: z
|
|
702
|
+
.string()
|
|
703
|
+
.regex(STELLAR_ACCOUNT_ADDRESS, 'sourceAccount must be a Stellar account address (G...)'),
|
|
590
704
|
/** Target network for the revoke. Same `testnet`-default as install,
|
|
591
705
|
* so pre-mainnet callers keep working without an explicit flag. */
|
|
592
706
|
network: NetworkSchema.optional(),
|
|
@@ -610,8 +724,15 @@ export const GetInterpreterInfoInputSchema = z.object({
|
|
|
610
724
|
/** When true, perform an optional live `grammar_version()` RPC call to
|
|
611
725
|
* verify the deployed contract matches the pin. */
|
|
612
726
|
verifyLive: z.boolean().optional(),
|
|
613
|
-
/** Optional RPC URL override.
|
|
727
|
+
/** Optional RPC URL override. When `verifyLive` is true, the URL must
|
|
728
|
+
* equal the pinned RPC for the selected network unless
|
|
729
|
+
* `allowUnpinnedRpcUrl: true` is also set (same opt-in shape as
|
|
730
|
+
* install/revoke - the live grammar_version() answer binds to whichever
|
|
731
|
+
* RPC returned it). */
|
|
614
732
|
rpcUrl: z.string().url().optional(),
|
|
733
|
+
/** Opt-in to using a non-pinned RPC URL when `verifyLive` is true. Same
|
|
734
|
+
* semantics as install/revoke: the caller accepts the trust shift. */
|
|
735
|
+
allowUnpinnedRpcUrl: z.boolean().optional(),
|
|
615
736
|
})
|
|
616
737
|
export type GetInterpreterInfoInput = z.infer<typeof GetInterpreterInfoInputSchema>
|
|
617
738
|
|
|
@@ -1,52 +1,42 @@
|
|
|
1
|
-
// src/synth/compose-from-recording.ts - facts + scope -> PolicyIR (OZ
|
|
2
|
-
//
|
|
3
|
-
// Composes the canonical IR rules the two backends compile FROM. Fail-closed
|
|
4
|
-
// composition rules:
|
|
5
|
-
//
|
|
6
|
-
// - identify the protocol of the top-level call (registry.identifyProtocol).
|
|
7
|
-
// When it is unknown (null), emit NO OZ-primitive-producing IR node: the
|
|
8
|
-
// scope is kept (CallContract + method) and every inferred bound is surfaced
|
|
9
|
-
// as a descriptive warning. An unrecognised call never compiles to a
|
|
10
|
-
// permissive OZ primitive.
|
|
1
|
+
// src/synth/compose-from-recording.ts - facts + scope -> PolicyIR (OZ + interpreter).
|
|
11
2
|
//
|
|
3
|
+
// Fail-closed composition rules:
|
|
4
|
+
// - unknown top-level protocol (registry.identifyProtocol returns null) ->
|
|
5
|
+
// emit no OZ-primitive-producing IR node; scope is kept (CallContract + method)
|
|
6
|
+
// and every inferred bound surfaces as a descriptive warning. An unrecognised
|
|
7
|
+
// call never compiles to a permissive OZ primitive.
|
|
12
8
|
// - carry the recorded top-level function into `rule.scope.method` so the OZ
|
|
13
9
|
// adapter flags per-method scoping as not covered (CallContract permits every
|
|
14
|
-
// method
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// window
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// -
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// fabricated count.
|
|
29
|
-
//
|
|
30
|
-
// - the IR carries ONLY constraints justified by the recording (observed
|
|
31
|
-
// recipient allowlist) + explicit user input. Nothing invented: no oracle
|
|
32
|
-
// price fabricated from a slippage bound, no synthetic exact-path compare.
|
|
33
|
-
// Those needs are surfaced as descriptive warnings instead.
|
|
10
|
+
// method; a per-method restriction needs the interpreter predicate).
|
|
11
|
+
// - `spending_limit` (window_spent(token, w) <= limit) is emitted ONLY when
|
|
12
|
+
// the caller supplies BOTH limit (userResponses.limitAmount) AND window
|
|
13
|
+
// (userResponses.windowSeconds). A single recorded spend does NOT authorise
|
|
14
|
+
// that amount every window, so the observed amount is NEVER used as an
|
|
15
|
+
// auto-ceiling: missing limit -> AMOUNT_BOUND_MISSING (observed amount
|
|
16
|
+
// offered as a suggestion); missing window -> DURATION_UNSPECIFIED. EVERY
|
|
17
|
+
// spent token is handled - none is silently dropped.
|
|
18
|
+
// - incoming-only flows emit an `invocation_count` bound ONLY when the caller
|
|
19
|
+
// supplies both the count and the window; otherwise FREQUENCY_BOUND_MISSING
|
|
20
|
+
// with no fabricated count.
|
|
21
|
+
// - the IR carries ONLY constraints justified by the recording + explicit
|
|
22
|
+
// user input. Nothing invented: no oracle price fabricated from a slippage
|
|
23
|
+
// bound, no synthetic exact-path compare. Those needs surface as warnings.
|
|
34
24
|
//
|
|
35
25
|
// Split rule (P3 wiring): `ComposeResult` carries BOTH `ir` (OZ-shape) and
|
|
36
26
|
// `interpreterIr` (predicate-shape). Each constraint is routed to EXACTLY ONE
|
|
37
27
|
// adapter:
|
|
38
|
-
// - `
|
|
39
|
-
//
|
|
40
|
-
// - everything else
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
28
|
+
// - `window_spent(token, w) <= limit` where `token === scope.contract` and
|
|
29
|
+
// protocol is known -> `ir` (OZ lowers to spending_limit).
|
|
30
|
+
// - everything else (recipient allowlists, per-method scope.method,
|
|
31
|
+
// invocation_count, eq_seq swap paths, oracle_price, AND window_spent
|
|
32
|
+
// where token != scope.contract i.e. SoroSwap input-token cap) ->
|
|
33
|
+
// `interpreterIr`.
|
|
44
34
|
//
|
|
45
35
|
// This prevents the interpreter adapter from emitting a duplicate
|
|
46
36
|
// `window_spent` predicate leaf alongside an OZ `spending_limit` primitive
|
|
47
37
|
// covering the same spend semantic - the two adapters never overlap.
|
|
48
38
|
//
|
|
49
|
-
//
|
|
39
|
+
// Default policy is `deny_all` (OZ context rules are deny-by-default).
|
|
50
40
|
|
|
51
41
|
import type { IRCompOp, IRCondition, IRPolicyRule, PolicyIR } from '../ir/types.ts'
|
|
52
42
|
import { type IdentifiedProtocol, identifyProtocol } from '../registry/identify.ts'
|
|
@@ -81,25 +71,20 @@ export interface ComposeUserResponses {
|
|
|
81
71
|
* invocation_count bound; absent -> FREQUENCY_BOUND_MISSING. */
|
|
82
72
|
invocationLimit?: number
|
|
83
73
|
/** Per-asset oracle-price bound(s). Each entry lowers to a single
|
|
84
|
-
* `oracle_price(asset) OP value` compare in the interpreter IR.
|
|
85
|
-
* entries on the same asset emit multiple leaves. */
|
|
74
|
+
* `oracle_price(asset) OP value` compare in the interpreter IR. */
|
|
86
75
|
oraclePriceBound?: OraclePriceBound[]
|
|
87
76
|
/** Minimum acceptable swap output per unit of input, as `num/den` (e.g.
|
|
88
|
-
* `{num:'95',den:'100'}` = accept losing at most 5%).
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* recording. The recorded in/out pair is a price at one moment, and
|
|
94
|
-
* freezing it as policy would deny ordinary trades as soon as the rate
|
|
95
|
-
* moves. Absent, no floor is emitted and the existing unbounded-output
|
|
96
|
-
* warning stands. */
|
|
77
|
+
* `{num:'95',den:'100'}` = accept losing at most 5%). REQUIRED to be
|
|
78
|
+
* supplied by the caller: never derived from the recording (the recorded
|
|
79
|
+
* in/out pair is a price at one moment, and freezing it as policy would
|
|
80
|
+
* deny ordinary trades as soon as the rate moves). Absent, no floor is
|
|
81
|
+
* emitted and the existing unbounded-output warning stands. */
|
|
97
82
|
swapMinOutRatio?: { num: string; den: string }
|
|
98
83
|
/** Recipient allowlist for a swap (call_arg[3] on SoroSwap's
|
|
99
|
-
* swap_exact_tokens_for_tokens). When supplied,
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
84
|
+
* swap_exact_tokens_for_tokens). When supplied, REPLACES the default pin.
|
|
85
|
+
* Absent -> recipient is pinned to the recorded value (mirroring SEP-41)
|
|
86
|
+
* and RECIPIENT_ALLOWLIST_EMPTY surfaces as informational, never a silent
|
|
87
|
+
* free pass. */
|
|
103
88
|
swapRecipientAllowlist?: string[]
|
|
104
89
|
}
|
|
105
90
|
|
|
@@ -144,6 +129,13 @@ export function composeFromRecording(
|
|
|
144
129
|
const warnings: string[] = []
|
|
145
130
|
const ozConstraints: IRCondition[] = []
|
|
146
131
|
const interpreterConstraints: IRCondition[] = []
|
|
132
|
+
// Route to the matching IR. When the interpreter is enabled, constraints the
|
|
133
|
+
// OZ adapter cannot lower go there; otherwise they go to OZ (which flags them
|
|
134
|
+
// as uncovered) so today's warning-driven behaviour is preserved.
|
|
135
|
+
const routeToAdapter = (cond: IRCondition): void => {
|
|
136
|
+
if (interpreterEnabled) interpreterConstraints.push(cond)
|
|
137
|
+
else ozConstraints.push(cond)
|
|
138
|
+
}
|
|
147
139
|
|
|
148
140
|
const protocol = topLevel
|
|
149
141
|
? identifyProtocol(topLevel.contract, topLevel.fn, topLevel.args, opts.network)
|
|
@@ -154,14 +146,15 @@ export function composeFromRecording(
|
|
|
154
146
|
const limitAmount = opts.userResponses?.limitAmount
|
|
155
147
|
const spendTokens = Object.keys(facts.spendByToken)
|
|
156
148
|
|
|
157
|
-
// Outgoing spend -> one spending_limit per spent token. A single caller
|
|
158
|
-
// binds only an unambiguous single-token spend; a multi-token flow
|
|
159
|
-
// per-token limit, so each unmatched token surfaces
|
|
149
|
+
// Outgoing spend -> one spending_limit per spent token. A single caller
|
|
150
|
+
// limit binds only an unambiguous single-token spend; a multi-token flow
|
|
151
|
+
// needs a per-token limit, so each unmatched token surfaces
|
|
152
|
+
// AMOUNT_BOUND_MISSING.
|
|
160
153
|
//
|
|
161
154
|
// Routing: a `window_spent(token, w) <= limit` constraint goes to the OZ IR
|
|
162
155
|
// only when token === scope.contract (OZ's spending_limit binds the
|
|
163
156
|
// CallContract target, not a token parameter). Otherwise it goes to the
|
|
164
|
-
// interpreter IR
|
|
157
|
+
// interpreter IR.
|
|
165
158
|
if (spendTokens.length > 0 && topLevel) {
|
|
166
159
|
let durationFlagged = false
|
|
167
160
|
for (const token of spendTokens) {
|
|
@@ -190,24 +183,23 @@ export function composeFromRecording(
|
|
|
190
183
|
})
|
|
191
184
|
continue
|
|
192
185
|
}
|
|
193
|
-
if (windowSeconds !== undefined
|
|
194
|
-
|
|
186
|
+
if (windowSeconds !== undefined) {
|
|
187
|
+
// Rolling spend cap always goes to OZ (`spending_limit` is the audited
|
|
188
|
+
// implementation). The interpreter is NOT a fallback for token !=
|
|
189
|
+
// scopeContract: on chain it sees one authorized call, not the
|
|
190
|
+
// transaction's token movements, so it has no per-call amount to
|
|
191
|
+
// accumulate and the counter would never move. OZ reports the case it
|
|
192
|
+
// cannot cover (limit pins to the context contract) - the honest
|
|
193
|
+
// outcome; the old fallback produced an interpreter predicate that
|
|
194
|
+
// silently never bound.
|
|
195
|
+
ozConstraints.push({
|
|
195
196
|
op: 'compare',
|
|
196
197
|
compare: {
|
|
197
198
|
selector: { kind: 'window_spent', token, windowSeconds },
|
|
198
199
|
operator: 'lte',
|
|
199
200
|
value: limit,
|
|
200
201
|
},
|
|
201
|
-
}
|
|
202
|
-
// A rolling spend cap always goes to OZ, whose `spending_limit` is the
|
|
203
|
-
// audited implementation. The interpreter is NOT a fallback for the
|
|
204
|
-
// token != scopeContract case: on chain it sees one authorized call,
|
|
205
|
-
// not the transaction's token movements, so it has no per-call amount
|
|
206
|
-
// to accumulate and the counter would never move. OZ reports the case
|
|
207
|
-
// it cannot cover (it pins the limit to the context contract), which
|
|
208
|
-
// is the honest outcome - the old fallback produced an interpreter
|
|
209
|
-
// predicate that silently never bound.
|
|
210
|
-
ozConstraints.push(spendCond)
|
|
202
|
+
})
|
|
211
203
|
if (interpreterEnabled && token !== scopeContract) {
|
|
212
204
|
warnings.push(
|
|
213
205
|
`rolling spend cap on ${token} cannot be enforced on chain: OZ spending_limit pins the limit to the context contract, and the interpreter cannot observe token movements. Bound the per-call value with an argument cap plus an invocation-count limit instead.`
|
|
@@ -222,18 +214,18 @@ export function composeFromRecording(
|
|
|
222
214
|
// Routed to the interpreter IR when interpreter is enabled (OZ cannot lower
|
|
223
215
|
// invocation_count); otherwise to the OZ IR (which flags it as uncovered).
|
|
224
216
|
//
|
|
225
|
-
// A recognised swap is NOT an incoming-only flow: it has an outgoing input
|
|
226
|
-
// whose spend simply was not attributed to the source account
|
|
227
|
-
// fee-sponsored / holder != source
|
|
228
|
-
// recipient,
|
|
229
|
-
//
|
|
217
|
+
// A recognised swap is NOT an incoming-only flow: it has an outgoing input
|
|
218
|
+
// leg whose spend simply was not attributed to the source account
|
|
219
|
+
// (fee-sponsored / holder != source). Its real restrictions - exact path,
|
|
220
|
+
// recipient, input-amount cap - come from the protocol-specific pass, so it
|
|
221
|
+
// does NOT get the incoming-only frequency prompt. A caller wanting to
|
|
230
222
|
// rate-limit the swap can still supply an invocationLimit + window, which
|
|
231
223
|
// lowers to an invocation_count for any flow.
|
|
232
224
|
if (spendTokens.length === 0 && topLevel) {
|
|
233
225
|
const invocationLimit = opts.userResponses?.invocationLimit
|
|
234
226
|
const isRecognisedSwap = protocol?.protocol === 'soroswap'
|
|
235
227
|
if (known && windowSeconds !== undefined && invocationLimit !== undefined) {
|
|
236
|
-
|
|
228
|
+
routeToAdapter({
|
|
237
229
|
op: 'compare',
|
|
238
230
|
compare: {
|
|
239
231
|
selector: { kind: 'invocation_count', windowSeconds },
|
|
@@ -243,12 +235,7 @@ export function composeFromRecording(
|
|
|
243
235
|
operator: 'lt',
|
|
244
236
|
value: String(invocationLimit),
|
|
245
237
|
},
|
|
246
|
-
}
|
|
247
|
-
if (interpreterEnabled) {
|
|
248
|
-
interpreterConstraints.push(icCond)
|
|
249
|
-
} else {
|
|
250
|
-
ozConstraints.push(icCond)
|
|
251
|
-
}
|
|
238
|
+
})
|
|
252
239
|
} else if (!isRecognisedSwap) {
|
|
253
240
|
ambiguities.push({
|
|
254
241
|
code: 'FREQUENCY_BOUND_MISSING',
|
|
@@ -266,7 +253,7 @@ export function composeFromRecording(
|
|
|
266
253
|
const oracleBounds = opts.userResponses?.oraclePriceBound
|
|
267
254
|
if (oracleBounds) {
|
|
268
255
|
for (const b of oracleBounds) {
|
|
269
|
-
|
|
256
|
+
routeToAdapter({
|
|
270
257
|
op: 'compare',
|
|
271
258
|
compare: {
|
|
272
259
|
selector: { kind: 'oracle_price', asset: b.asset },
|
|
@@ -274,12 +261,7 @@ export function composeFromRecording(
|
|
|
274
261
|
value: b.value,
|
|
275
262
|
valueDecimals: b.decimals,
|
|
276
263
|
},
|
|
277
|
-
}
|
|
278
|
-
if (interpreterEnabled) {
|
|
279
|
-
interpreterConstraints.push(oracleCond)
|
|
280
|
-
} else {
|
|
281
|
-
ozConstraints.push(oracleCond)
|
|
282
|
-
}
|
|
264
|
+
})
|
|
283
265
|
}
|
|
284
266
|
}
|
|
285
267
|
|
|
@@ -296,8 +278,8 @@ export function composeFromRecording(
|
|
|
296
278
|
// arg cap is skipped to avoid binding one limit to two different semantics.
|
|
297
279
|
const swapInputAmountCap = spendTokens.length === 0 ? limitAmount : undefined
|
|
298
280
|
appendProtocolSpecificConstraints(
|
|
299
|
-
ozConstraints,
|
|
300
281
|
interpreterConstraints,
|
|
282
|
+
routeToAdapter,
|
|
301
283
|
warnings,
|
|
302
284
|
ambiguities,
|
|
303
285
|
facts,
|
|
@@ -353,8 +335,8 @@ export function composeFromRecording(
|
|
|
353
335
|
* (oraclePriceBound + limitAmount) + the recorded path (eq_seq on
|
|
354
336
|
* call_arg[2]). */
|
|
355
337
|
function appendProtocolSpecificConstraints(
|
|
356
|
-
ozConstraints: IRCondition[],
|
|
357
338
|
interpreterConstraints: IRCondition[],
|
|
339
|
+
routeToAdapter: (cond: IRCondition) => void,
|
|
358
340
|
warnings: string[],
|
|
359
341
|
ambiguities: AmbiguityPrompt[],
|
|
360
342
|
facts: IntentFacts,
|
|
@@ -365,31 +347,24 @@ function appendProtocolSpecificConstraints(
|
|
|
365
347
|
swapMinOutRatio: { num: string; den: string } | undefined,
|
|
366
348
|
interpreterEnabled: boolean
|
|
367
349
|
): void {
|
|
368
|
-
// SEP-41 transfer / mint: the `to` arg (index 1) is the recipient. Emit
|
|
369
|
-
//
|
|
350
|
+
// SEP-41 transfer / mint: the `to` arg (index 1) is the recipient. Emit a
|
|
351
|
+
// single-element allowlist; the interpreter adapter lowers it to `in`.
|
|
370
352
|
// When interpreter is not enabled, route to OZ so the caller sees today's
|
|
371
|
-
// `value allowlist on arg 1
|
|
353
|
+
// `value allowlist on arg 1` warning.
|
|
372
354
|
if (protocol.protocol === 'sep41' && (protocol.fn === 'transfer' || protocol.fn === 'mint')) {
|
|
373
355
|
const toArg = topLevel.args[1]
|
|
374
356
|
if (toArg && toArg.type === 'address') {
|
|
375
|
-
|
|
357
|
+
routeToAdapter({
|
|
376
358
|
op: 'in',
|
|
377
359
|
selector: { kind: 'arg', argIndex: 1, scalarType: 'address' },
|
|
378
360
|
values: [toArg.value],
|
|
379
|
-
}
|
|
380
|
-
if (interpreterEnabled) {
|
|
381
|
-
interpreterConstraints.push(cond)
|
|
382
|
-
} else {
|
|
383
|
-
ozConstraints.push(cond)
|
|
384
|
-
}
|
|
361
|
+
})
|
|
385
362
|
}
|
|
386
363
|
}
|
|
387
364
|
|
|
388
365
|
// Blend submit / claim: the `to` arg is the beneficiary - it receives the
|
|
389
366
|
// resulting position shares on `submit` and the claimed tokens on `claim`.
|
|
390
|
-
//
|
|
391
|
-
// recipient below, so it is pinned to the recorded address the same way:
|
|
392
|
-
// without it a policy scoped to a pool and method still lets an agent send
|
|
367
|
+
// Without it, a policy scoped to a pool + method still lets an agent send
|
|
393
368
|
// the proceeds anywhere. `from` and `spender` are deliberately NOT pinned -
|
|
394
369
|
// the call already requires their authorisation, so binding them adds no
|
|
395
370
|
// restriction the chain is not already enforcing.
|
|
@@ -398,25 +373,20 @@ function appendProtocolSpecificConstraints(
|
|
|
398
373
|
// claim(from, reserve_token_ids, to).
|
|
399
374
|
const toArg = topLevel.args[2]
|
|
400
375
|
if (toArg && toArg.type === 'address') {
|
|
401
|
-
|
|
376
|
+
routeToAdapter({
|
|
402
377
|
op: 'in',
|
|
403
378
|
selector: { kind: 'arg', argIndex: 2, scalarType: 'address' },
|
|
404
379
|
values: [toArg.value],
|
|
405
|
-
}
|
|
406
|
-
if (interpreterEnabled) {
|
|
407
|
-
interpreterConstraints.push(cond)
|
|
408
|
-
} else {
|
|
409
|
-
ozConstraints.push(cond)
|
|
410
|
-
}
|
|
380
|
+
})
|
|
411
381
|
}
|
|
412
382
|
}
|
|
413
383
|
|
|
414
384
|
// Blend `submit` ONLY (not `claim`, whose vec arg is a vec<u32> of
|
|
415
385
|
// reserve_token_ids - no map fields to bind). The `requests` vec
|
|
416
386
|
// (call_arg[3]) is a vec<Request{ address, amount, request_type }>. Each
|
|
417
|
-
// Request is the per-reserve action selector
|
|
387
|
+
// Request is the per-reserve action selector (0 Supply, 1 Withdraw,
|
|
418
388
|
// 2 SupplyCollateral, 3 WithdrawCollateral, 4 Borrow, 5 Repay, 6-9
|
|
419
|
-
// liquidation/auction fills. Pinning only one element is unsafe: a caller
|
|
389
|
+
// liquidation/auction fills). Pinning only one element is unsafe: a caller
|
|
420
390
|
// can append a second element with a different action (WithdrawCollateral
|
|
421
391
|
// -> Borrow on a different asset, any amount, then auction fills). Length
|
|
422
392
|
// + per-element pinning is total; a quantifier over elements is not. If we
|
|
@@ -565,19 +535,14 @@ function appendProtocolSpecificConstraints(
|
|
|
565
535
|
inputAmountArg &&
|
|
566
536
|
inputAmountArg.type === 'i128'
|
|
567
537
|
) {
|
|
568
|
-
|
|
538
|
+
routeToAdapter({
|
|
569
539
|
op: 'compare',
|
|
570
540
|
compare: {
|
|
571
541
|
selector: { kind: 'arg', argIndex: inputArgIndex, scalarType: 'i128' },
|
|
572
542
|
operator: 'lte',
|
|
573
543
|
value: swapInputAmountCap,
|
|
574
544
|
},
|
|
575
|
-
}
|
|
576
|
-
if (interpreterEnabled) {
|
|
577
|
-
interpreterConstraints.push(cond)
|
|
578
|
-
} else {
|
|
579
|
-
ozConstraints.push(cond)
|
|
580
|
-
}
|
|
545
|
+
})
|
|
581
546
|
}
|
|
582
547
|
|
|
583
548
|
// Slippage floor: `out >= in * num/den`. Only when the caller supplied the
|
|
@@ -592,18 +557,13 @@ function appendProtocolSpecificConstraints(
|
|
|
592
557
|
inputAmountArg &&
|
|
593
558
|
inputAmountArg.type === 'i128'
|
|
594
559
|
) {
|
|
595
|
-
|
|
560
|
+
routeToAdapter({
|
|
596
561
|
op: 'slippage_floor',
|
|
597
562
|
outArgIndex: outMinArgIndex,
|
|
598
563
|
inArgIndex: inputArgIndex,
|
|
599
564
|
num: minOutRatio.num,
|
|
600
565
|
den: minOutRatio.den,
|
|
601
|
-
}
|
|
602
|
-
if (interpreterEnabled) {
|
|
603
|
-
interpreterConstraints.push(floor)
|
|
604
|
-
} else {
|
|
605
|
-
ozConstraints.push(floor)
|
|
606
|
-
}
|
|
566
|
+
})
|
|
607
567
|
}
|
|
608
568
|
|
|
609
569
|
// Swap recipient (call_arg[3]): when the caller supplies
|
|
@@ -620,16 +580,11 @@ function appendProtocolSpecificConstraints(
|
|
|
620
580
|
// (today's behaviour, matching the other SoroSwap constraints).
|
|
621
581
|
const recipientArg = topLevel.args[3]
|
|
622
582
|
if (swapRecipientAllowlist && swapRecipientAllowlist.length > 0) {
|
|
623
|
-
|
|
583
|
+
routeToAdapter({
|
|
624
584
|
op: 'in',
|
|
625
585
|
selector: { kind: 'arg', argIndex: 3, scalarType: 'address' },
|
|
626
586
|
values: [...swapRecipientAllowlist],
|
|
627
|
-
}
|
|
628
|
-
if (interpreterEnabled) {
|
|
629
|
-
interpreterConstraints.push(cond)
|
|
630
|
-
} else {
|
|
631
|
-
ozConstraints.push(cond)
|
|
632
|
-
}
|
|
587
|
+
})
|
|
633
588
|
} else if (interpreterEnabled && recipientArg && recipientArg.type === 'address') {
|
|
634
589
|
interpreterConstraints.push({
|
|
635
590
|
op: 'in',
|