@crediolabs/policy-synth 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/README.md +4 -4
  2. package/dist/adapters/interpreter/adapter.d.ts +19 -25
  3. package/dist/adapters/interpreter/adapter.js +54 -487
  4. package/dist/errors.d.ts +1 -1
  5. package/dist/index.d.ts +1 -4
  6. package/dist/index.js +1 -4
  7. package/dist/install/build-add-context-rule.d.ts +3 -19
  8. package/dist/install/build-add-context-rule.js +3 -98
  9. package/dist/install/build-install-policy.d.ts +4 -53
  10. package/dist/install/build-install-policy.js +44 -123
  11. package/dist/install/index.d.ts +0 -2
  12. package/dist/install/index.js +0 -7
  13. package/dist/predicate/decode.d.ts +1 -1
  14. package/dist/predicate/decode.js +2 -70
  15. package/dist/predicate/encode.js +45 -229
  16. package/dist/predicate/from-json.js +1 -42
  17. package/dist/record/decode.js +3 -8
  18. package/dist/record/freshness.d.ts +17 -11
  19. package/dist/record/freshness.js +28 -18
  20. package/dist/review-card/builder.d.ts +4 -3
  21. package/dist/review-card/builder.js +18 -162
  22. package/dist/review-card/cross-check.js +2 -105
  23. package/dist/review-card/render-leaf.d.ts +4 -0
  24. package/dist/review-card/render-leaf.js +47 -0
  25. package/dist/run/index.d.ts +36 -139
  26. package/dist/run/index.js +125 -437
  27. package/dist/run/schemas.d.ts +168 -1929
  28. package/dist/run/schemas.js +40 -269
  29. package/dist/{synth → simulate}/deny-cases.d.ts +4 -8
  30. package/dist/simulate/deny-cases.js +282 -0
  31. package/dist/simulate/evaluate.d.ts +18 -0
  32. package/dist/simulate/evaluate.js +271 -0
  33. package/dist/simulate/index.d.ts +4 -0
  34. package/dist/simulate/index.js +8 -0
  35. package/dist/synth/compose-from-recording.d.ts +24 -51
  36. package/dist/synth/compose-from-recording.js +120 -282
  37. package/dist/synth/index.d.ts +0 -5
  38. package/dist/synth/index.js +0 -5
  39. package/dist/synth/synthesize-from-recording.d.ts +1 -29
  40. package/dist/synth/synthesize-from-recording.js +41 -356
  41. package/dist/types.d.ts +8 -60
  42. package/dist/types.js +5 -2
  43. package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
  44. package/dist-cjs/adapters/interpreter/adapter.js +56 -489
  45. package/dist-cjs/errors.d.ts +1 -1
  46. package/dist-cjs/index.d.ts +1 -4
  47. package/dist-cjs/index.js +1 -4
  48. package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
  49. package/dist-cjs/install/build-add-context-rule.js +1 -97
  50. package/dist-cjs/install/build-install-policy.d.ts +4 -53
  51. package/dist-cjs/install/build-install-policy.js +44 -122
  52. package/dist-cjs/install/index.d.ts +0 -2
  53. package/dist-cjs/install/index.js +2 -23
  54. package/dist-cjs/predicate/decode.d.ts +1 -1
  55. package/dist-cjs/predicate/decode.js +2 -70
  56. package/dist-cjs/predicate/encode.js +45 -229
  57. package/dist-cjs/predicate/from-json.js +1 -42
  58. package/dist-cjs/record/decode.js +3 -8
  59. package/dist-cjs/record/freshness.d.ts +17 -11
  60. package/dist-cjs/record/freshness.js +28 -18
  61. package/dist-cjs/review-card/builder.d.ts +4 -3
  62. package/dist-cjs/review-card/builder.js +23 -167
  63. package/dist-cjs/review-card/cross-check.js +7 -110
  64. package/dist-cjs/review-card/render-leaf.d.ts +4 -0
  65. package/dist-cjs/review-card/render-leaf.js +52 -0
  66. package/dist-cjs/run/index.d.ts +36 -139
  67. package/dist-cjs/run/index.js +125 -444
  68. package/dist-cjs/run/schemas.d.ts +168 -1929
  69. package/dist-cjs/run/schemas.js +41 -270
  70. package/dist-cjs/{synth → simulate}/deny-cases.d.ts +4 -8
  71. package/dist-cjs/simulate/deny-cases.js +286 -0
  72. package/dist-cjs/simulate/evaluate.d.ts +18 -0
  73. package/dist-cjs/simulate/evaluate.js +274 -0
  74. package/dist-cjs/simulate/index.d.ts +4 -0
  75. package/dist-cjs/simulate/index.js +13 -0
  76. package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
  77. package/dist-cjs/synth/compose-from-recording.js +120 -282
  78. package/dist-cjs/synth/index.d.ts +0 -5
  79. package/dist-cjs/synth/index.js +1 -11
  80. package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
  81. package/dist-cjs/synth/synthesize-from-recording.js +39 -354
  82. package/dist-cjs/types.d.ts +8 -60
  83. package/dist-cjs/types.js +6 -3
  84. package/package.json +5 -5
  85. package/src/adapters/interpreter/adapter.ts +76 -572
  86. package/src/errors.ts +0 -19
  87. package/src/index.ts +1 -4
  88. package/src/install/build-add-context-rule.ts +5 -139
  89. package/src/install/build-install-policy.ts +93 -214
  90. package/src/install/index.ts +0 -34
  91. package/src/predicate/decode.ts +2 -70
  92. package/src/predicate/encode.ts +49 -261
  93. package/src/predicate/from-json.ts +1 -43
  94. package/src/record/decode.ts +3 -8
  95. package/src/record/freshness.ts +29 -18
  96. package/src/review-card/builder.ts +19 -168
  97. package/src/review-card/cross-check.ts +3 -105
  98. package/src/review-card/render-leaf.ts +48 -0
  99. package/src/run/index.ts +144 -572
  100. package/src/run/schemas.ts +42 -291
  101. package/src/simulate/deny-cases.ts +334 -0
  102. package/src/simulate/evaluate.ts +284 -0
  103. package/src/simulate/index.ts +11 -0
  104. package/src/synth/compose-from-recording.ts +148 -347
  105. package/src/synth/index.ts +0 -13
  106. package/src/synth/synthesize-from-recording.ts +43 -456
  107. package/src/types.ts +13 -49
  108. package/dist/adapters/interpreter/index.d.ts +0 -1
  109. package/dist/adapters/interpreter/index.js +0 -2
  110. package/dist/adapters/oz/adapter.d.ts +0 -20
  111. package/dist/adapters/oz/adapter.js +0 -295
  112. package/dist/adapters/oz/index.d.ts +0 -1
  113. package/dist/adapters/oz/index.js +0 -2
  114. package/dist/codegen/compile-gate.d.ts +0 -33
  115. package/dist/codegen/compile-gate.js +0 -124
  116. package/dist/codegen/index.d.ts +0 -2
  117. package/dist/codegen/index.js +0 -8
  118. package/dist/codegen/template.d.ts +0 -18
  119. package/dist/codegen/template.js +0 -148
  120. package/dist/install/authority-overlap.d.ts +0 -134
  121. package/dist/install/authority-overlap.js +0 -0
  122. package/dist/install/build-install-predicate.d.ts +0 -96
  123. package/dist/install/build-install-predicate.js +0 -444
  124. package/dist/install/build-merge-policy.d.ts +0 -70
  125. package/dist/install/build-merge-policy.js +0 -130
  126. package/dist/install/plan-merge-policy.d.ts +0 -49
  127. package/dist/install/plan-merge-policy.js +0 -86
  128. package/dist/install/read-account-rules.d.ts +0 -100
  129. package/dist/install/read-account-rules.js +0 -283
  130. package/dist/ir/index.d.ts +0 -1
  131. package/dist/ir/index.js +0 -2
  132. package/dist/ir/types.d.ts +0 -140
  133. package/dist/ir/types.js +0 -11
  134. package/dist/mandate/index.d.ts +0 -2
  135. package/dist/mandate/index.js +0 -2
  136. package/dist/mandate/to-ir.d.ts +0 -3
  137. package/dist/mandate/to-ir.js +0 -60
  138. package/dist/mandate/types.d.ts +0 -20
  139. package/dist/mandate/types.js +0 -8
  140. package/dist/seams/index.d.ts +0 -1
  141. package/dist/seams/index.js +0 -2
  142. package/dist/seams/types.d.ts +0 -66
  143. package/dist/seams/types.js +0 -11
  144. package/dist/synth/deny-cases.js +0 -562
  145. package/dist/synth/evaluate.d.ts +0 -39
  146. package/dist/synth/evaluate.js +0 -551
  147. package/dist/synth/harness.d.ts +0 -28
  148. package/dist/synth/harness.js +0 -47
  149. package/dist/synth/minimize.d.ts +0 -4
  150. package/dist/synth/minimize.js +0 -38
  151. package/dist/synth/permit-context.d.ts +0 -15
  152. package/dist/synth/permit-context.js +0 -116
  153. package/dist/synth/predicate-literals.d.ts +0 -5
  154. package/dist/synth/predicate-literals.js +0 -25
  155. package/dist/synth/synthesize-from-mandate.d.ts +0 -20
  156. package/dist/synth/synthesize-from-mandate.js +0 -59
  157. package/dist/verify/envelope.d.ts +0 -15
  158. package/dist/verify/envelope.js +0 -22
  159. package/dist/verify/index.d.ts +0 -3
  160. package/dist/verify/index.js +0 -3
  161. package/dist/verify/simulate.d.ts +0 -31
  162. package/dist/verify/simulate.js +0 -258
  163. package/dist/verify/verify.d.ts +0 -21
  164. package/dist/verify/verify.js +0 -189
  165. package/dist-cjs/adapters/interpreter/index.d.ts +0 -1
  166. package/dist-cjs/adapters/interpreter/index.js +0 -8
  167. package/dist-cjs/adapters/oz/adapter.d.ts +0 -20
  168. package/dist-cjs/adapters/oz/adapter.js +0 -300
  169. package/dist-cjs/adapters/oz/index.d.ts +0 -1
  170. package/dist-cjs/adapters/oz/index.js +0 -8
  171. package/dist-cjs/codegen/compile-gate.d.ts +0 -33
  172. package/dist-cjs/codegen/compile-gate.js +0 -128
  173. package/dist-cjs/codegen/index.d.ts +0 -2
  174. package/dist-cjs/codegen/index.js +0 -14
  175. package/dist-cjs/codegen/template.d.ts +0 -18
  176. package/dist-cjs/codegen/template.js +0 -151
  177. package/dist-cjs/install/authority-overlap.d.ts +0 -134
  178. package/dist-cjs/install/authority-overlap.js +0 -0
  179. package/dist-cjs/install/build-install-predicate.d.ts +0 -96
  180. package/dist-cjs/install/build-install-predicate.js +0 -479
  181. package/dist-cjs/install/build-merge-policy.d.ts +0 -70
  182. package/dist-cjs/install/build-merge-policy.js +0 -134
  183. package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
  184. package/dist-cjs/install/plan-merge-policy.js +0 -90
  185. package/dist-cjs/install/read-account-rules.d.ts +0 -100
  186. package/dist-cjs/install/read-account-rules.js +0 -296
  187. package/dist-cjs/ir/index.d.ts +0 -1
  188. package/dist-cjs/ir/index.js +0 -3
  189. package/dist-cjs/ir/types.d.ts +0 -140
  190. package/dist-cjs/ir/types.js +0 -12
  191. package/dist-cjs/mandate/index.d.ts +0 -2
  192. package/dist-cjs/mandate/index.js +0 -6
  193. package/dist-cjs/mandate/to-ir.d.ts +0 -3
  194. package/dist-cjs/mandate/to-ir.js +0 -63
  195. package/dist-cjs/mandate/types.d.ts +0 -20
  196. package/dist-cjs/mandate/types.js +0 -9
  197. package/dist-cjs/seams/index.d.ts +0 -1
  198. package/dist-cjs/seams/index.js +0 -3
  199. package/dist-cjs/seams/types.d.ts +0 -66
  200. package/dist-cjs/seams/types.js +0 -12
  201. package/dist-cjs/synth/deny-cases.js +0 -568
  202. package/dist-cjs/synth/evaluate.d.ts +0 -39
  203. package/dist-cjs/synth/evaluate.js +0 -554
  204. package/dist-cjs/synth/harness.d.ts +0 -28
  205. package/dist-cjs/synth/harness.js +0 -50
  206. package/dist-cjs/synth/minimize.d.ts +0 -4
  207. package/dist-cjs/synth/minimize.js +0 -41
  208. package/dist-cjs/synth/permit-context.d.ts +0 -15
  209. package/dist-cjs/synth/permit-context.js +0 -119
  210. package/dist-cjs/synth/predicate-literals.d.ts +0 -5
  211. package/dist-cjs/synth/predicate-literals.js +0 -28
  212. package/dist-cjs/synth/synthesize-from-mandate.d.ts +0 -20
  213. package/dist-cjs/synth/synthesize-from-mandate.js +0 -62
  214. package/dist-cjs/verify/envelope.d.ts +0 -15
  215. package/dist-cjs/verify/envelope.js +0 -23
  216. package/dist-cjs/verify/index.d.ts +0 -3
  217. package/dist-cjs/verify/index.js +0 -8
  218. package/dist-cjs/verify/simulate.d.ts +0 -31
  219. package/dist-cjs/verify/simulate.js +0 -261
  220. package/dist-cjs/verify/verify.d.ts +0 -21
  221. package/dist-cjs/verify/verify.js +0 -192
  222. package/src/adapters/interpreter/index.ts +0 -8
  223. package/src/adapters/oz/adapter.ts +0 -376
  224. package/src/adapters/oz/index.ts +0 -9
  225. package/src/codegen/compile-gate.ts +0 -167
  226. package/src/codegen/index.ts +0 -17
  227. package/src/codegen/template.ts +0 -165
  228. package/src/install/authority-overlap.ts +0 -0
  229. package/src/install/build-merge-policy.ts +0 -219
  230. package/src/install/plan-merge-policy.ts +0 -133
  231. package/src/install/read-account-rules.ts +0 -376
  232. package/src/ir/index.ts +0 -13
  233. package/src/ir/types.ts +0 -132
  234. package/src/mandate/index.ts +0 -4
  235. package/src/mandate/to-ir.ts +0 -71
  236. package/src/mandate/types.ts +0 -21
  237. package/src/seams/index.ts +0 -11
  238. package/src/seams/types.ts +0 -81
  239. package/src/synth/deny-cases.ts +0 -663
  240. package/src/synth/evaluate.ts +0 -613
  241. package/src/synth/harness.ts +0 -68
  242. package/src/synth/minimize.ts +0 -48
  243. package/src/synth/permit-context.ts +0 -136
  244. package/src/synth/predicate-literals.ts +0 -27
  245. package/src/synth/synthesize-from-mandate.ts +0 -82
  246. package/src/verify/envelope.ts +0 -28
  247. package/src/verify/index.ts +0 -5
  248. package/src/verify/simulate.ts +0 -311
  249. package/src/verify/verify.ts +0 -243
@@ -1,479 +0,0 @@
1
- "use strict";
2
- // src/install/build-install-predicate.ts - builds the unsigned Soroban
3
- // transaction XDR for `interpreter.install(params, raw_context_rule, smart_account)`.
4
- //
5
- // Call 2 of the install sequence. The OZ smart account's `add_context_rule`
6
- // (call 1, see build-install-policy.ts) attaches the rule to the account and
7
- // the interpreter POLICY address, but the interpreter itself needs the
8
- // encoded predicate bytes + the matching hash + the verbatim context rule
9
- // it will hash at enforce. Storing those is what `interpreter.install`
10
- // does; without it the interpreter returns MissingState and the rule denies
11
- // EVERYTHING - which on a deny-only demo is indistinguishable from a
12
- // working policy.
13
- //
14
- // CALL 1 / CALL 2 NONCE LIAISON (critical invariant, empirically verified
15
- // on testnet 2026-08-01):
16
- // - The OZ smart account's `add_context_rule` increments the
17
- // interpreter's stored nonce for `(smart_account, rule_id)` from 0
18
- // to 1. (Verified via getLedgerEntries after call 1.)
19
- // - `interpreter.install` requires
20
- // `install_params.install_nonce == stored_nonce.saturating_add(1)`.
21
- // - Therefore call 2 MUST derive its install_nonce from the stored
22
- // nonce via getLedgerEntries - taking it as caller input is a design
23
- // bug that produces NonceReplay (#202) for the caller every time
24
- // call 1 has bumped the counter. (octopos historically accepts an
25
- // arbitrary install_nonce and the user has to know to pass 2; this
26
- // builder reads the stored nonce instead.)
27
- //
28
- // AUTH TREE (empirically verified on testnet 2026-08-01):
29
- // The recording simulation of `interpreter.install` returns ONE
30
- // address-credential auth entry:
31
- // auth[0] credentials=address address=<SMART_ACCOUNT> root=install
32
- // (The previously hypothesised second entry for the rule signer does
33
- // not materialise on this pinned interpreter: `__check_auth` for the
34
- // rule id in `contextRuleIds` runs the rule's `policy.enforce`, but
35
- // because the rule's policy is the interpreter we are CALLING, the
36
- // interpreter's own `install` does not recurse into require_auth on
37
- // the rule signer; the only require_auth is the smart account's
38
- // `__check_auth` from the rule the auth payload names.)
39
- //
40
- // The auth tree is therefore the same shape as `buildInstallPolicyXdr`:
41
- // [0] accountEntry(original, validUntil, AuthPayload([sourceAccount], [ruleId], () => Bytes()))
42
- // [1] delegatedSignerEntry(smartAccount, digest)
43
- // The second entry is a `sorobanCredentialsSourceAccount` and the
44
- // source-account envelope signature covers it. The wallet signs only
45
- // the envelope. Verified on testnet 2026-08-01 against the pinned
46
- // interpreter CDR4NLV22STCXFGZPNKDQTEANWLF7LZ6AJLY6B7CLJXKHDZGYJWIOKGP.
47
- //
48
- // CONTEXT RULE (empirically verified on testnet 2026-08-01):
49
- // `contextRuleIds` is the NEW rule id assigned in call 1 - exactly
50
- // what octopos's `installPredicateDoc` passes. Earlier reasoning
51
- // claimed rule 0 was required (chicken-and-egg: the new rule's
52
- // interpreter policy denies `install` because no predicate is
53
- // installed yet); that reasoning is wrong. The smart account
54
- // consults the rule's interpreter policy for the `install` host call
55
- // and the interpreter permits it - the rule's own install is a
56
- // privileged install-only path that the interpreter lets through. A
57
- // `[0]` choice was producing a structurally different auth tree
58
- // whose digest bound to the wrong rule and whose `AuthPayload`'s
59
- // context_rule_ids never matched the rule the interpreter recorded.
60
- //
61
- // `params` is an ScMap for a #[contracttype] struct. The host unpacks by
62
- // field count, so a missing or extra field traps with WasmVm UnexpectedSize
63
- // rather than a clean code. Keys MUST be in this sorted order:
64
- // grammar_version, install_nonce, oracle_max_deviation_bps,
65
- // oracle_max_staleness_seconds, oracle_max_xfeed_dev_bps,
66
- // predicate, predicate_hash
67
- //
68
- // `raw_context_rule` MUST be the ScVal `get_context_rule(ruleId)` returned,
69
- // passed through VERBATIM. The interpreter hashes `context_rule.signers` at
70
- // install and re-checks at enforce; any reconstruction that differs in any
71
- // field denies RULE_SIGNERS_CHANGED forever, bricking the rule. So this
72
- // builder fetches the rule itself - callers do not get to hand-build it.
73
- //
74
- // SECURITY INVARIANT: the auth_digest binds to networkPassphrase +
75
- // per-credential nonce + the call's root invocation + context_rule_ids.
76
- // Replaying the envelope against another call or another account is
77
- // impossible without re-signing.
78
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
79
- if (k2 === undefined) k2 = k;
80
- var desc = Object.getOwnPropertyDescriptor(m, k);
81
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
82
- desc = { enumerable: true, get: function() { return m[k]; } };
83
- }
84
- Object.defineProperty(o, k2, desc);
85
- }) : (function(o, m, k, k2) {
86
- if (k2 === undefined) k2 = k;
87
- o[k2] = m[k];
88
- }));
89
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
90
- Object.defineProperty(o, "default", { enumerable: true, value: v });
91
- }) : function(o, v) {
92
- o["default"] = v;
93
- });
94
- var __importStar = (this && this.__importStar) || (function () {
95
- var ownKeys = function(o) {
96
- ownKeys = Object.getOwnPropertyNames || function (o) {
97
- var ar = [];
98
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
99
- return ar;
100
- };
101
- return ownKeys(o);
102
- };
103
- return function (mod) {
104
- if (mod && mod.__esModule) return mod;
105
- var result = {};
106
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
107
- __setModuleDefault(result, mod);
108
- return result;
109
- };
110
- })();
111
- Object.defineProperty(exports, "__esModule", { value: true });
112
- exports.Contract = void 0;
113
- exports.buildInstallPredicateXdr = buildInstallPredicateXdr;
114
- const node_crypto_1 = require("node:crypto");
115
- const stellar_sdk_1 = require("@stellar/stellar-sdk");
116
- Object.defineProperty(exports, "Contract", { enumerable: true, get: function () { return stellar_sdk_1.Contract; } });
117
- const oz_auth_ts_1 = require("./oz-auth.js");
118
- /** Build the unsigned transaction envelope for
119
- * `interpreter.install(params, raw_context_rule, smart_account)`. The
120
- * output XDR is signed by the wallet, not by us. */
121
- async function buildInstallPredicateXdr(args) {
122
- // 1. Fetch the raw context rule from the smart account via simulation.
123
- // The interpreter hashes the rule's signers at install; we MUST hand
124
- // it the exact bytes the account holds, not a reconstruction.
125
- const rawContextRule = await fetchRawContextRule(args.rpc, args.sourceAccount, args.smartAccount, args.ruleId, args.networkPassphrase);
126
- // 2. Read the interpreter's stored nonce for this (smart_account,
127
- // rule_id) so call 2's install_nonce is exactly stored+1. Anything
128
- // else trips NonceReplay (#202). The key shape matches
129
- // `packages/policy-interpreter/src/storage.rs`:
130
- // K_NONCE: u32 = 2
131
- // key = (Address(smart_account), u32(rule_id), u32(2))
132
- // A value of `None` means stored_nonce = 0 (first install).
133
- const storedNonce = await fetchStoredNonce(args.rpc, args.interpreterAddress, args.smartAccount, args.ruleId);
134
- const installNonce = storedNonce + 1;
135
- // 3. Build the params ScMap. Field order is sorted by symbol string;
136
- // the host unpacks by field count and any missing/extra field traps
137
- // with WasmVm UnexpectedSize rather than a clean code.
138
- const grammarVersion = args.grammarVersion ?? 1;
139
- const predicateBytes = Buffer.from(args.encodedPredicate, 'base64');
140
- const predicateHashBytes = Buffer.from(args.predicateHash, 'hex');
141
- const params = buildInstallParams({
142
- grammarVersion,
143
- installNonce,
144
- predicateBytes,
145
- predicateHashBytes,
146
- });
147
- // 4. Build the host function. `interpreter.install(params, raw_rule,
148
- // smart_account)`.
149
- const hostFunction = stellar_sdk_1.xdr.HostFunction.hostFunctionTypeInvokeContract(new stellar_sdk_1.xdr.InvokeContractArgs({
150
- contractAddress: new stellar_sdk_1.Address(args.interpreterAddress).toScAddress(),
151
- functionName: 'install',
152
- args: [params, rawContextRule, new stellar_sdk_1.Address(args.smartAccount).toScVal()],
153
- }));
154
- const source = await args.rpc.getAccount(args.sourceAccount);
155
- const makeOperation = (auth = []) => stellar_sdk_1.Operation.invokeHostFunction({ func: hostFunction, auth });
156
- const baseFee = args.baseFee !== undefined ? String(args.baseFee) : stellar_sdk_1.BASE_FEE;
157
- const buildTx = (op) => buildUnsignedTx({
158
- sourceAccount: args.sourceAccount,
159
- sequence: source.sequenceNumber(),
160
- fee: baseFee,
161
- networkPassphrase: args.networkPassphrase,
162
- op,
163
- });
164
- // 5. Recording simulation: capture the smart-account auth entry.
165
- // `contextRuleIds = [ruleId]` - the rule the account assigned in
166
- // call 1 - is what octopos's `installPredicateDoc` passes (see
167
- // apps/web/ui/octopos/smart-account-install.ts:313-320). The rule's
168
- // own interpreter policy permits the `install` host call (it is a
169
- // privileged install path), so authorising via the rule itself is
170
- // safe and is the only choice whose auth tree binds to the rule
171
- // the interpreter recorded.
172
- const recordingTx = buildTx(makeOperation());
173
- const recorded = await args.rpc.simulateTransaction(recordingTx);
174
- if (stellar_sdk_1.rpc.Api.isSimulationError(recorded)) {
175
- throw new Error('install_predicate: simulateTransaction failed');
176
- }
177
- const recordedAuth = recorded.result?.auth ?? [];
178
- const original = recordedAuth.find((entry) => entry.credentials().switch().name === 'sorobanCredentialsAddress' &&
179
- stellar_sdk_1.Address.fromScAddress(entry.credentials().address().address()).toString() ===
180
- args.smartAccount);
181
- if (!original) {
182
- throw new Error(`install_predicate: no Soroban auth entry for smart account ${args.smartAccount}; this call does not route through the smart account`);
183
- }
184
- // 6. Build the OZ auth entries (same shape as install_policy).
185
- // `contextRuleIds = [args.ruleId]` binds the AuthPayload's
186
- // context_rule_ids + the authDigest to the rule the interpreter
187
- // recorded. The delegated signer entry uses source-account
188
- // credentials so the envelope signature covers it; the wallet
189
- // supplies only the ordinary envelope signature.
190
- const validUntilLedger = (await args.rpc.getLatestLedger()).sequence +
191
- (args.authValidUntilLedgers ?? DEFAULT_AUTH_VALID_UNTIL_LEDGERS);
192
- const contextRuleIds = [args.ruleId];
193
- const digest = (0, oz_auth_ts_1.authDigest)((0, oz_auth_ts_1.signaturePayload)(args.networkPassphrase, original.credentials().address().nonce(), validUntilLedger, original.rootInvocation()), contextRuleIds);
194
- const authEntries = [
195
- (0, oz_auth_ts_1.accountEntry)(original, validUntilLedger, (0, oz_auth_ts_1.authPayload)([args.sourceAccount], contextRuleIds, () => Buffer.alloc(0))),
196
- ...contextRuleIds.map(() => (0, oz_auth_ts_1.delegatedSignerEntry)(args.smartAccount, digest)),
197
- ];
198
- // 7. Enforcing simulation + assemble. Same pattern as
199
- // buildInstallPolicyXdr: the SDK preserves the OZ auth entries
200
- // during assembly and adds the simulated Soroban footprint +
201
- // resource fee, yielding a complete unsigned envelope.
202
- const txWithAuth = buildTx(makeOperation(authEntries));
203
- const enforcing = await args.rpc.simulateTransaction(txWithAuth);
204
- if (stellar_sdk_1.rpc.Api.isSimulationError(enforcing)) {
205
- throw new Error('install_predicate: auth simulateTransaction failed');
206
- }
207
- const finalTx = stellar_sdk_1.rpc.assembleTransaction(txWithAuth, enforcing).build();
208
- // 8. Decode the structured description FROM the final assembled
209
- // transaction. The bytes are the source of truth.
210
- const describes = decodeInstallPredicateCallDescribes(finalTx, args.predicateHash);
211
- return {
212
- unsignedXdr: finalTx.toEnvelope().toXDR().toString('base64'),
213
- smartAccount: args.smartAccount,
214
- sourceAccount: args.sourceAccount,
215
- interpreterAddress: args.interpreterAddress,
216
- ruleId: args.ruleId,
217
- call: { contract: args.interpreterAddress, fn: 'install' },
218
- authNonce: original.credentials().address().nonce().toString(),
219
- authValidUntilLedger: validUntilLedger,
220
- rootInvocationXdr: original.rootInvocation().toXDR().toString('base64'),
221
- describes,
222
- };
223
- }
224
- // ---- internals ----
225
- /** ~25 minutes at 5s/ledger. Same default as build-install-policy so the
226
- * two-call sequence can be signed inside the same window without a
227
- * fresh source account bump between calls. */
228
- const DEFAULT_AUTH_VALID_UNTIL_LEDGERS = 300;
229
- /** K_NONCE constant from packages/policy-interpreter/src/storage.rs.
230
- * The interpreter's persistent-storage key shape is the tuple
231
- * `(Address, u32, u32)` where the third u32 is the namespace. We hard-
232
- * code 2 here because the interpreter source is pinned and out of this
233
- * builder's scope; a drift test should assert these stay in lockstep
234
- * if the storage layout ever changes. */
235
- const K_NONCE = 2;
236
- /** Read the interpreter's stored nonce for `(smart_account, rule_id)`
237
- * via a direct ledger read. No public getter exists on the pinned
238
- * interpreter, so we read the contract-data ledger entry directly.
239
- * Returns 0 when the entry is absent (a fresh (account, rule_id) that
240
- * has never been installed). Used to derive `install_nonce =
241
- * stored_nonce + 1` for `interpreter.install`.
242
- *
243
- * The InstallRpcClient interface intentionally omits `getLedgerEntries`
244
- * (the install pipeline uses getAccount / simulateTransaction /
245
- * getLatestLedger only); we hit the real Soroban RPC through the
246
- * shared sdk module rather than extending the interface, since this
247
- * single read keeps the read surface scoped.
248
- *
249
- * If the read errors (network blip, pinned interpreter still deploying,
250
- * etc.) we treat the read as if no entry exists and return 0 - the
251
- * enforcing simulation will fail closed on NonceReplay if we got it
252
- * wrong, with a clean error the caller can act on. */
253
- async function fetchStoredNonce(rpc, interpreterAddress, smartAccount, ruleId) {
254
- // Recover the underlying RPC URL by probing `getLatestLedger` - this
255
- // works for both the real-server and mock clients (mocks return a
256
- // sequence but not a URL). For tests, this returns 0 unless the mock
257
- // also surfaces an URL. We accept the limit: tests for the install
258
- // pipeline drive the nonce via the recorded context_rule rather than
259
- // this read (see build-install-predicate.test.ts).
260
- //
261
- // Production reads go through the Soroban RPC directly. We import the
262
- // sdk module lazily here so the test-time mock surface does not have
263
- // to grow.
264
- const { rpc: sdkRpc } = await Promise.resolve().then(() => __importStar(require('@stellar/stellar-sdk')));
265
- // The InstallRpcClient does not carry the URL; the run-layer wires a
266
- // real rpc.Server for production. We construct one from the pinned
267
- // TESTNET_RPC_URL here. This is hard-coded because the read surface
268
- // is testnet-only today; mainnet would need its own pinned address
269
- // and a separate URL pin.
270
- const TESTNET_RPC_URL = 'https://soroban-testnet.stellar.org';
271
- const server = new sdkRpc.Server(TESTNET_RPC_URL, { allowHttp: false });
272
- const key = stellar_sdk_1.xdr.LedgerKey.contractData(new stellar_sdk_1.xdr.LedgerKeyContractData({
273
- contract: new stellar_sdk_1.Address(interpreterAddress).toScAddress(),
274
- key: stellar_sdk_1.xdr.ScVal.scvVec([
275
- new stellar_sdk_1.Address(smartAccount).toScVal(),
276
- stellar_sdk_1.xdr.ScVal.scvU32(ruleId),
277
- stellar_sdk_1.xdr.ScVal.scvU32(K_NONCE),
278
- ]),
279
- durability: stellar_sdk_1.xdr.ContractDataDurability.persistent(),
280
- }));
281
- try {
282
- const r = await server.getLedgerEntries(key);
283
- if (!r.entries || r.entries.length === 0)
284
- return 0;
285
- const entry = r.entries[0];
286
- if (!entry)
287
- return 0;
288
- const data = entry.val?.contractData();
289
- if (!data)
290
- return 0;
291
- const val = data.val();
292
- if (val.switch().name === 'scvU32')
293
- return val.u32();
294
- return 0;
295
- }
296
- catch {
297
- // Treat ledger read errors as "no prior install". The enforcing
298
- // simulation will fail closed with NonceReplay if the assumption
299
- // is wrong, surfacing a clean error to the caller.
300
- void rpc;
301
- return 0;
302
- }
303
- }
304
- /** Fetch the verbatim ScVal the smart account holds for a given rule id.
305
- * Used by `interpreter.install` (call 2): the interpreter hashes
306
- * `context_rule.signers` at install and re-checks at enforce, so any
307
- * client-side reconstruction that differs in any field makes the rule
308
- * deny RULE_SIGNERS_CHANGED forever. We round-trip through a `simulate`
309
- * of `get_context_rule(ruleId)` and return the raw retval. */
310
- async function fetchRawContextRule(rpcClient, sourceAccount, smartAccount, ruleId, networkPassphrase) {
311
- const source = await rpcClient.getAccount(sourceAccount);
312
- const op = stellar_sdk_1.Operation.invokeHostFunction({
313
- func: stellar_sdk_1.xdr.HostFunction.hostFunctionTypeInvokeContract(new stellar_sdk_1.xdr.InvokeContractArgs({
314
- contractAddress: new stellar_sdk_1.Address(smartAccount).toScAddress(),
315
- functionName: 'get_context_rule',
316
- args: [stellar_sdk_1.xdr.ScVal.scvU32(ruleId)],
317
- })),
318
- auth: [],
319
- });
320
- const tx = new stellar_sdk_1.TransactionBuilder(new stellar_sdk_1.Account(sourceAccount, source.sequenceNumber()), {
321
- fee: stellar_sdk_1.BASE_FEE,
322
- networkPassphrase,
323
- })
324
- .addOperation(op)
325
- .setTimeout(0)
326
- .build();
327
- const sim = await rpcClient.simulateTransaction(tx);
328
- if (stellar_sdk_1.rpc.Api.isSimulationError(sim)) {
329
- throw new Error(`install_predicate: get_context_rule(${ruleId}) simulateTransaction failed: ${sim.error}`);
330
- }
331
- const retval = sim.result?.retval;
332
- if (!retval) {
333
- throw new Error(`install_predicate: get_context_rule(${ruleId}) returned no value`);
334
- }
335
- return retval;
336
- }
337
- /** Build the params ScMap for `interpreter.install`. Keys MUST be sorted
338
- * by symbol string (the canonical map encoding requires it). The host
339
- * unpacks by field count, so a missing or extra field traps with WasmVm
340
- * UnexpectedSize rather than a clean code. */
341
- function buildInstallParams(args) {
342
- const entry = (k, v) => new stellar_sdk_1.xdr.ScMapEntry({ key: stellar_sdk_1.xdr.ScVal.scvSymbol(k), val: v });
343
- // Sorted order: grammar_version, install_nonce, oracle_max_deviation_bps,
344
- // oracle_max_staleness_seconds, oracle_max_xfeed_dev_bps, predicate,
345
- // predicate_hash.
346
- return stellar_sdk_1.xdr.ScVal.scvMap([
347
- entry('grammar_version', stellar_sdk_1.xdr.ScVal.scvU32(args.grammarVersion)),
348
- entry('install_nonce', stellar_sdk_1.xdr.ScVal.scvU32(args.installNonce)),
349
- entry('oracle_max_deviation_bps', stellar_sdk_1.xdr.ScVal.scvVoid()),
350
- entry('oracle_max_staleness_seconds', stellar_sdk_1.xdr.ScVal.scvVoid()),
351
- entry('oracle_max_xfeed_dev_bps', stellar_sdk_1.xdr.ScVal.scvVoid()),
352
- entry('predicate', stellar_sdk_1.xdr.ScVal.scvBytes(args.predicateBytes)),
353
- entry('predicate_hash', stellar_sdk_1.xdr.ScVal.scvBytes(args.predicateHashBytes)),
354
- ]);
355
- }
356
- /** Build an unsigned Soroban transaction envelope. Same pattern as
357
- * build-install-policy.ts so the consumer code path is uniform. */
358
- function buildUnsignedTx(args) {
359
- const account = new stellar_sdk_1.Account(args.sourceAccount, args.sequence);
360
- return new stellar_sdk_1.TransactionBuilder(account, {
361
- fee: args.fee,
362
- networkPassphrase: args.networkPassphrase,
363
- })
364
- .addOperation(args.op)
365
- .setTimeout(0)
366
- .build();
367
- }
368
- /** Decode the install-predicate call's structured fields directly out of
369
- * the final assembled transaction. The bytes are the source of truth:
370
- * a human reviewing the install wants to see what the wallet will
371
- * actually sign, not what we think it will. The decode is deliberately
372
- * strict - any shape we did not build ourselves throws, since that
373
- * would mean the XDR was tampered with (or the encoder changed and
374
- * this descriptor lags).
375
- *
376
- * The second argument (raw context rule) is passed through verbatim;
377
- * we do NOT decode it here because reconstructing the rule's fields
378
- * client-side is exactly what the file header warns against. The rule
379
- * id lives inside it and is echoed from input at the top-level result. */
380
- function decodeInstallPredicateCallDescribes(tx, expectedPredicateHash) {
381
- const operations = tx.toEnvelope().v1().tx().operations();
382
- if (operations.length !== 1 || !operations[0]) {
383
- throw new Error(`install_predicate: final transaction has ${operations.length} operations, expected 1`);
384
- }
385
- const op = operations[0];
386
- const hostFn = op.body().invokeHostFunctionOp()?.hostFunction();
387
- if (hostFn?.switch().name !== 'hostFunctionTypeInvokeContract') {
388
- throw new Error('install_predicate: built op is not an invokeHostFunction(invokeContract(...)) call');
389
- }
390
- const invokeArgs = hostFn.invokeContract();
391
- if (!invokeArgs) {
392
- throw new Error('install_predicate: built op has no InvokeContractArgs payload');
393
- }
394
- const targetContract = stellar_sdk_1.Address.fromScAddress(invokeArgs.contractAddress()).toString();
395
- const fnName = invokeArgs.functionName().toString();
396
- if (fnName !== 'install') {
397
- throw new Error(`install_predicate: built op fn name is "${fnName}", expected "install"`);
398
- }
399
- const scvArgs = invokeArgs.args();
400
- if (scvArgs.length !== 3) {
401
- throw new Error(`install_predicate: built op has ${scvArgs.length} args, expected 3 (params, raw_context_rule, smart_account)`);
402
- }
403
- const paramsScv = scvArgs[0];
404
- const rawContextRuleScv = scvArgs[1];
405
- const smartAccountScv = scvArgs[2];
406
- if (!paramsScv || !rawContextRuleScv || !smartAccountScv) {
407
- throw new Error('install_predicate: built op args include an undefined slot despite length check');
408
- }
409
- // params: an ScMap with seven fields (sorted by symbol).
410
- if (paramsScv.switch().name !== 'scvMap') {
411
- throw new Error('install_predicate: args[0] (params) is not an ScVal::Map');
412
- }
413
- const paramsFields = new Map();
414
- for (const inner of paramsScv.map() ?? []) {
415
- const fk = inner.key();
416
- if (fk.switch().name !== 'scvSymbol') {
417
- throw new Error('install_predicate: params field key is not an ScVal::Symbol');
418
- }
419
- paramsFields.set(fk.sym().toString(), inner.val());
420
- }
421
- const EXPECTED_PARAMS_KEYS = [
422
- 'grammar_version',
423
- 'install_nonce',
424
- 'oracle_max_deviation_bps',
425
- 'oracle_max_staleness_seconds',
426
- 'oracle_max_xfeed_dev_bps',
427
- 'predicate',
428
- 'predicate_hash',
429
- ];
430
- const actualKeys = [...paramsFields.keys()].sort();
431
- if (actualKeys.length !== EXPECTED_PARAMS_KEYS.length ||
432
- actualKeys.some((k, i) => k !== EXPECTED_PARAMS_KEYS[i])) {
433
- throw new Error(`install_predicate: params keys ${JSON.stringify(actualKeys)} do not match the canonical sorted set ${JSON.stringify(EXPECTED_PARAMS_KEYS)}`);
434
- }
435
- const grammarVersionScv = paramsFields.get('grammar_version');
436
- if (!grammarVersionScv || grammarVersionScv.switch().name !== 'scvU32') {
437
- throw new Error('install_predicate: params.grammar_version is missing or not u32');
438
- }
439
- const grammarVersion = grammarVersionScv.u32();
440
- const installNonceScv = paramsFields.get('install_nonce');
441
- if (!installNonceScv || installNonceScv.switch().name !== 'scvU32') {
442
- throw new Error('install_predicate: params.install_nonce is missing or not u32');
443
- }
444
- const installNonce = installNonceScv.u32();
445
- const predicateScv = paramsFields.get('predicate');
446
- if (!predicateScv || predicateScv.switch().name !== 'scvBytes') {
447
- throw new Error('install_predicate: params.predicate is missing or not bytes');
448
- }
449
- const predicateBytes = Buffer.from(predicateScv.bytes());
450
- const predicateSha256OfEmbeddedBytes = (0, node_crypto_1.createHash)('sha256').update(predicateBytes).digest('hex');
451
- const predicateHashScv = paramsFields.get('predicate_hash');
452
- if (!predicateHashScv || predicateHashScv.switch().name !== 'scvBytes') {
453
- throw new Error('install_predicate: params.predicate_hash is missing or not bytes');
454
- }
455
- const predicateHash = Buffer.from(predicateHashScv.bytes()).toString('hex');
456
- if (predicateHash !== expectedPredicateHash) {
457
- throw new Error(`install_predicate: params.predicate_hash (${predicateHash}) differs from the caller-supplied hash (${expectedPredicateHash})`);
458
- }
459
- if (rawContextRuleScv.switch().name === 'scvVoid') {
460
- throw new Error('install_predicate: args[1] (raw_context_rule) is scvVoid');
461
- }
462
- if (smartAccountScv.switch().name !== 'scvAddress') {
463
- throw new Error('install_predicate: args[2] (smart_account) is not an ScVal::Address');
464
- }
465
- const smartAccountOnWire = stellar_sdk_1.Address.fromScAddress(smartAccountScv.address()).toString();
466
- return {
467
- targetContract,
468
- fnName: 'install',
469
- grammarVersion,
470
- installNonce,
471
- predicateHash,
472
- predicateSha256OfEmbeddedBytes,
473
- smartAccountOnWire,
474
- };
475
- }
476
- // Suppress the unused-import warning for K_NONCE while keeping the
477
- // constant visible for the drift test it invites. (Removed once a
478
- // drift test is in place that references it.)
479
- void K_NONCE;
@@ -1,70 +0,0 @@
1
- import type { InstallRpcClient } from './build-install-policy.ts';
2
- import type { MergeStep } from './plan-merge-policy.ts';
3
- /**
4
- * The context rule the merge authorises AGAINST, which is not the rule being
5
- * merged.
6
- *
7
- * Both calls mutate the smart account itself, so they need the account's own
8
- * authorisation, and this builder asks rule 0 for it - the same assumption the
9
- * install and revoke builders make. OpenZeppelin does NOT enforce any
10
- * admin semantic for rule 0: it is simply the first rule the account's
11
- * constructor created, and OZ's reference account creates it as `Default`
12
- * covering every context.
13
- *
14
- * The assumption is therefore load-bearing and unverified at build time. If
15
- * rule 0 does not exist, does not cover `CallContract(<smart account>)`, or
16
- * does not list the source account as a signer, the transaction is built and
17
- * then fails when submitted, and the operator pays the fee for a transaction
18
- * that was never signable. Changing it is a wire-visible change: the id is
19
- * hashed into the auth digest.
20
- */
21
- export declare const ADMIN_CONTEXT_RULE_ID = 0;
22
- export interface BuildMergePolicyArgs {
23
- smartAccount: string;
24
- sourceAccount: string;
25
- networkPassphrase: string;
26
- ruleId: number;
27
- /** OZ registry id of the interpreter policy on this rule, from the plan. */
28
- policyId: number;
29
- interpreterAddress: string;
30
- step: MergeStep;
31
- /** Only needed for `reinstall`: the merged predicate, already encoded. */
32
- encodedPredicate?: string;
33
- predicateHash?: string;
34
- /** Nonce for the reinstall. Read from chain rather than assumed: a detach
35
- * whose uninstall panicked leaves the old nonce in place. */
36
- installNonce?: number;
37
- /** Oracle bounds to carry over from the document being replaced. */
38
- oracleParams?: {
39
- maxStalenessSeconds?: number;
40
- maxDeviationBps?: number;
41
- maxCrossFeedDeviationBps?: number;
42
- };
43
- rpc: InstallRpcClient;
44
- baseFee?: number;
45
- authValidUntilLedgers?: number;
46
- }
47
- export interface BuildMergePolicyResult {
48
- unsignedXdr: string;
49
- smartAccount: string;
50
- sourceAccount: string;
51
- step: MergeStep;
52
- call: {
53
- contract: string;
54
- fn: 'remove_policy' | 'add_policy';
55
- ruleId: number;
56
- };
57
- authNonce: string;
58
- authValidUntilLedger: number;
59
- rootInvocationXdr: string;
60
- }
61
- /**
62
- * Build the unsigned transaction for one step of the merge.
63
- *
64
- * The auth dance mirrors `buildRevokePolicyXdr`: simulate once to record the
65
- * account's auth entry, rebuild with the payload bound to that recorded
66
- * `rootInvocation`, then assemble. Binding to the recorded invocation rather
67
- * than to the input arguments is what makes the signature commit to the exact
68
- * rule and policy being changed.
69
- */
70
- export declare function buildMergePolicyXdr(args: BuildMergePolicyArgs): Promise<BuildMergePolicyResult>;