@crediolabs/policy-synth 0.1.13 → 0.1.15

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.
@@ -14,9 +14,12 @@
14
14
  // (b) key material the server does not have, so we ship the simpler ONE-CALL
15
15
  // shape. The wallet signature covers the change.
16
16
  //
17
- // `buildInstallPolicyXdr` returns CALL 1 ONLY. Call 2 (interpreter.install)
18
- // needs the rule id the account assigns in call 1, so it cannot be pre-built
19
- // and is documented in the response envelope under `followUp`.
17
+ // `buildInstallPolicyXdr` installs the policy in ONE call. `add_context_rule`
18
+ // takes `policies` as a `Map<policy_address, install_param>` and the account
19
+ // forwards each install_param to that policy, so the interpreter stores the
20
+ // predicate document as part of this same transaction. Earlier revisions
21
+ // documented a second `interpreter.install` call; that was wrong, and issuing
22
+ // it fails - the account re-enters the interpreter while it is mid-install.
20
23
  Object.defineProperty(exports, "__esModule", { value: true });
21
24
  exports.DEFAULT_GRAMMAR_VERSION = exports.Contract = void 0;
22
25
  exports.rpcClientFromServer = rpcClientFromServer;
@@ -27,6 +30,7 @@ const stellar_sdk_1 = require("@stellar/stellar-sdk");
27
30
  Object.defineProperty(exports, "Contract", { enumerable: true, get: function () { return stellar_sdk_1.Contract; } });
28
31
  const build_add_context_rule_ts_1 = require("./build-add-context-rule.js");
29
32
  Object.defineProperty(exports, "DEFAULT_GRAMMAR_VERSION", { enumerable: true, get: function () { return build_add_context_rule_ts_1.DEFAULT_GRAMMAR_VERSION; } });
33
+ const oz_auth_ts_1 = require("./oz-auth.js");
30
34
  /** Convert a real rpc.Server into the InstallRpcClient surface. The
31
35
  * `getContractVersion` lookup uses `simulateTransaction` against
32
36
  * `contract.call('grammar_version')` and decodes the returned u32.
@@ -70,7 +74,6 @@ function rpcClientFromServer(server, networkPassphrase) {
70
74
  },
71
75
  };
72
76
  }
73
- const FOLLOWUP_HINT = 'after this tx confirms, read the rule id via account.get_context_rules_count()-1 (or get the receipt events), then call runInstallPolicy a second time with the interpreter as the target and the rule id in context_rule_ids - OR hand-build the install() call directly with the OZ auth pattern.';
74
77
  /** Build the unsigned transaction envelope for `account.add_context_rule(...)`.
75
78
  * The output XDR is signed by the wallet, not by us. */
76
79
  async function buildInstallPolicyXdr(args) {
@@ -91,31 +94,28 @@ async function buildInstallPolicyXdr(args) {
91
94
  ...(args.oracleParams ? { oracleParams: args.oracleParams } : {}),
92
95
  ...(args.grammarVersion !== undefined ? { grammarVersion: args.grammarVersion } : {}),
93
96
  });
94
- // 2. Network pass: source account sequence + latest ledger.
97
+ // 2. Fetch the source sequence, then build the recording transaction with a
98
+ // bare host call. The recording pass assigns the smart-account auth nonce
99
+ // and root invocation needed to construct OZ's AuthPayload.
95
100
  const source = await args.rpc.getAccount(args.sourceAccount);
96
- const latest = await args.rpc.getLatestLedger();
97
- const validUntilLedger = latest.sequence + (args.authValidUntilLedgers ?? DEFAULT_AUTH_VALID_LEDGERS);
98
- // 3. Build the unsigned transaction envelope with the bare host call.
99
- const op = stellar_sdk_1.Operation.invokeHostFunction({
100
- func: stellar_sdk_1.xdr.HostFunction.hostFunctionTypeInvokeContract(new stellar_sdk_1.xdr.InvokeContractArgs({
101
- contractAddress: new stellar_sdk_1.Address(args.smartAccount).toScAddress(),
102
- functionName: 'add_context_rule',
103
- args: [...callArgs],
104
- })),
105
- auth: [],
106
- });
101
+ const hostFunction = stellar_sdk_1.xdr.HostFunction.hostFunctionTypeInvokeContract(new stellar_sdk_1.xdr.InvokeContractArgs({
102
+ contractAddress: new stellar_sdk_1.Address(args.smartAccount).toScAddress(),
103
+ functionName: 'add_context_rule',
104
+ args: [...callArgs],
105
+ }));
106
+ const makeOperation = (auth = []) => stellar_sdk_1.Operation.invokeHostFunction({ func: hostFunction, auth });
107
107
  const baseFee = args.baseFee !== undefined ? String(args.baseFee) : stellar_sdk_1.BASE_FEE;
108
- const tx = buildUnsignedTx({
108
+ const buildTx = (op) => buildUnsignedTx({
109
109
  sourceAccount: args.sourceAccount,
110
110
  sequence: source.sequenceNumber(),
111
111
  fee: baseFee,
112
112
  networkPassphrase: args.networkPassphrase,
113
113
  op,
114
114
  });
115
- // 4. Simulation pass. Returns the auth nonce + rootInvocation the wallet
116
- // signature binds to. We do NOT add the AuthPayload here - the wallet
117
- // injects auth entries using its own __check_auth flow.
118
- const recorded = await args.rpc.simulateTransaction(tx);
115
+ const recordingTx = buildTx(makeOperation());
116
+ // 3. Recording simulation: capture the smart account's nonce and invocation
117
+ // tree. Nothing is signed here.
118
+ const recorded = await args.rpc.simulateTransaction(recordingTx);
119
119
  if (stellar_sdk_1.rpc.Api.isSimulationError(recorded)) {
120
120
  // Short, stable reason. The full `simulateTransaction` error (which
121
121
  // carries host + URL detail) stays in the SDK's own logs - never
@@ -129,23 +129,35 @@ async function buildInstallPolicyXdr(args) {
129
129
  if (!original) {
130
130
  throw new Error(`install_policy: no Soroban auth entry for smart account ${args.smartAccount}; this call does not route through the smart account`);
131
131
  }
132
- // 5. Decode the structured description FROM the unsigned XDR. The human
133
- // approval binds to the bytes the wallet will sign; deriving this from
134
- // the input args would re-describe the caller's intent rather than the
135
- // transaction. Re-parse the envelope we just built so the description
136
- // reflects what the wallet actually signs, not what we thought it would.
137
- const describes = decodeInstallCallDescribes(tx, op, args.installNonce);
132
+ // 4. `add_context_rule` is authorised by the deploy-time admin rule (rule 0).
133
+ // The delegated signer uses source-account credentials, so its signature
134
+ // bytes stay empty and the ordinary transaction-envelope signature covers
135
+ // the call when the consumer signs it.
136
+ const validUntilLedger = (await args.rpc.getLatestLedger()).sequence +
137
+ (args.authValidUntilLedgers ?? DEFAULT_AUTH_VALID_UNTIL_LEDGERS);
138
+ const contextRuleIds = [0];
139
+ const digest = (0, oz_auth_ts_1.authDigest)((0, oz_auth_ts_1.signaturePayload)(args.networkPassphrase, original.credentials().address().nonce(), validUntilLedger, original.rootInvocation()), contextRuleIds);
140
+ const authEntries = [
141
+ (0, oz_auth_ts_1.accountEntry)(original, validUntilLedger, (0, oz_auth_ts_1.authPayload)([args.sourceAccount], contextRuleIds, () => Buffer.alloc(0))),
142
+ ...contextRuleIds.map(() => (0, oz_auth_ts_1.delegatedSignerEntry)(args.smartAccount, digest)),
143
+ ];
144
+ // 5. Simulate again with the OZ entries already attached. The SDK preserves
145
+ // existing auth during assembly and adds the simulated Soroban footprint +
146
+ // resource fee, yielding a complete unsigned envelope.
147
+ const txWithAuth = buildTx(makeOperation(authEntries));
148
+ const enforcing = await args.rpc.simulateTransaction(txWithAuth);
149
+ if (stellar_sdk_1.rpc.Api.isSimulationError(enforcing)) {
150
+ throw new Error('install_policy: auth simulateTransaction failed');
151
+ }
152
+ const finalTx = stellar_sdk_1.rpc.assembleTransaction(txWithAuth, enforcing).build();
153
+ // 6. Decode the structured description FROM the final assembled transaction.
154
+ // The human approval binds to the exact bytes the wallet will sign.
155
+ const describes = decodeInstallCallDescribes(finalTx, args.installNonce);
138
156
  return {
139
- unsignedXdr: tx.toEnvelope().toXDR().toString('base64'),
157
+ unsignedXdr: finalTx.toEnvelope().toXDR().toString('base64'),
140
158
  smartAccount: args.smartAccount,
141
159
  sourceAccount: args.sourceAccount,
142
160
  call: { contract: args.smartAccount, fn: 'add_context_rule' },
143
- followUp: {
144
- contract: 'interpreter',
145
- fn: 'install',
146
- requiresRuleIdFromCallOne: true,
147
- hint: FOLLOWUP_HINT,
148
- },
149
161
  describes,
150
162
  authNonce: original.credentials().address().nonce().toString(),
151
163
  authValidUntilLedger: validUntilLedger,
@@ -165,25 +177,21 @@ async function buildInstallPolicyXdr(args) {
165
177
  * be guessing at a contract it cannot read, and the chain is the authority. */
166
178
  async function buildRevokePolicyXdr(args) {
167
179
  const source = await args.rpc.getAccount(args.sourceAccount);
168
- const latest = await args.rpc.getLatestLedger();
169
- const validUntilLedger = latest.sequence + (args.authValidUntilLedgers ?? DEFAULT_AUTH_VALID_LEDGERS);
170
- const op = stellar_sdk_1.Operation.invokeHostFunction({
171
- func: stellar_sdk_1.xdr.HostFunction.hostFunctionTypeInvokeContract(new stellar_sdk_1.xdr.InvokeContractArgs({
172
- contractAddress: new stellar_sdk_1.Address(args.smartAccount).toScAddress(),
173
- functionName: 'remove_context_rule',
174
- args: [stellar_sdk_1.xdr.ScVal.scvU32(args.ruleId)],
175
- })),
176
- auth: [],
177
- });
180
+ const hostFunction = stellar_sdk_1.xdr.HostFunction.hostFunctionTypeInvokeContract(new stellar_sdk_1.xdr.InvokeContractArgs({
181
+ contractAddress: new stellar_sdk_1.Address(args.smartAccount).toScAddress(),
182
+ functionName: 'remove_context_rule',
183
+ args: [stellar_sdk_1.xdr.ScVal.scvU32(args.ruleId)],
184
+ }));
185
+ const makeOperation = (auth = []) => stellar_sdk_1.Operation.invokeHostFunction({ func: hostFunction, auth });
178
186
  const baseFee = args.baseFee !== undefined ? String(args.baseFee) : stellar_sdk_1.BASE_FEE;
179
- const tx = buildUnsignedTx({
187
+ const buildTx = (op) => buildUnsignedTx({
180
188
  sourceAccount: args.sourceAccount,
181
189
  sequence: source.sequenceNumber(),
182
190
  fee: baseFee,
183
191
  networkPassphrase: args.networkPassphrase,
184
192
  op,
185
193
  });
186
- const recorded = await args.rpc.simulateTransaction(tx);
194
+ const recorded = await args.rpc.simulateTransaction(buildTx(makeOperation()));
187
195
  if (stellar_sdk_1.rpc.Api.isSimulationError(recorded)) {
188
196
  // Short, stable reason. The full `simulateTransaction` error (which
189
197
  // carries host + URL detail) stays in the SDK's own logs - never
@@ -197,8 +205,27 @@ async function buildRevokePolicyXdr(args) {
197
205
  if (!original) {
198
206
  throw new Error(`revoke_policy: no Soroban auth entry for smart account ${args.smartAccount}; this call does not route through the smart account`);
199
207
  }
208
+ // Removal is master-only, so the deploy-time admin rule authorises it. The
209
+ // recorded rootInvocation still includes remove_context_rule(ruleId), binding
210
+ // the auth payload to the exact rule being removed. As with install,
211
+ // source-account credentials carry the delegated signer entry and the
212
+ // consumer supplies only the ordinary envelope signature.
213
+ const validUntilLedger = (await args.rpc.getLatestLedger()).sequence +
214
+ (args.authValidUntilLedgers ?? DEFAULT_AUTH_VALID_UNTIL_LEDGERS);
215
+ const contextRuleIds = [0];
216
+ const digest = (0, oz_auth_ts_1.authDigest)((0, oz_auth_ts_1.signaturePayload)(args.networkPassphrase, original.credentials().address().nonce(), validUntilLedger, original.rootInvocation()), contextRuleIds);
217
+ const authEntries = [
218
+ (0, oz_auth_ts_1.accountEntry)(original, validUntilLedger, (0, oz_auth_ts_1.authPayload)([args.sourceAccount], contextRuleIds, () => Buffer.alloc(0))),
219
+ ...contextRuleIds.map(() => (0, oz_auth_ts_1.delegatedSignerEntry)(args.smartAccount, digest)),
220
+ ];
221
+ const txWithAuth = buildTx(makeOperation(authEntries));
222
+ const enforcing = await args.rpc.simulateTransaction(txWithAuth);
223
+ if (stellar_sdk_1.rpc.Api.isSimulationError(enforcing)) {
224
+ throw new Error('revoke_policy: auth simulateTransaction failed');
225
+ }
226
+ const finalTx = stellar_sdk_1.rpc.assembleTransaction(txWithAuth, enforcing).build();
200
227
  return {
201
- unsignedXdr: tx.toEnvelope().toXDR().toString('base64'),
228
+ unsignedXdr: finalTx.toEnvelope().toXDR().toString('base64'),
202
229
  smartAccount: args.smartAccount,
203
230
  sourceAccount: args.sourceAccount,
204
231
  call: { contract: args.smartAccount, fn: 'remove_context_rule', ruleId: args.ruleId },
@@ -208,7 +235,7 @@ async function buildRevokePolicyXdr(args) {
208
235
  };
209
236
  }
210
237
  /** ~25 minutes at 5s/ledger. */
211
- const DEFAULT_AUTH_VALID_LEDGERS = 300;
238
+ const DEFAULT_AUTH_VALID_UNTIL_LEDGERS = 300;
212
239
  // ---- internals ----
213
240
  /** Adapter: turn the install-policy wire `PolicyRef` shape into the core
214
241
  * `PolicyRef` shape `buildAddContextRuleArgs` expects. Keeps the wire
@@ -246,20 +273,24 @@ function buildUnsignedTx(args) {
246
273
  // its signature, and broadcasts. We do NOT sign here.
247
274
  return tx;
248
275
  }
249
- /** Decode the install call's structured fields directly out of the
250
- * built (transaction, operation) pair. The bytes are the source of
251
- * truth: a human reviewing the install wants to see what the wallet
252
- * will actually sign, not what we think it will. The decode is
253
- * deliberately strict - any shape we did not build ourselves throws
254
- * a ToolError, since that would mean the XDR was tampered with (or
276
+ /** Decode the install call's structured fields directly out of the final
277
+ * assembled transaction. The bytes are the source of truth: a human reviewing
278
+ * the install wants to see what the wallet will actually sign, not what we
279
+ * think it will. The decode is deliberately strict - any shape we did not
280
+ * build ourselves throws, since that would mean the XDR was tampered with (or
255
281
  * the encoder changed and this descriptor lags).
256
282
  *
257
283
  * `expectedInstallNonce` is a fallback for the rare case where the
258
284
  * install has no interpreter policy (no `install_nonce` to read); the
259
285
  * caller already knows it. */
260
- function decodeInstallCallDescribes(tx, op, expectedInstallNonce) {
286
+ function decodeInstallCallDescribes(tx, expectedInstallNonce) {
287
+ const operations = tx.toEnvelope().v1().tx().operations();
288
+ if (operations.length !== 1 || !operations[0]) {
289
+ throw new Error(`install_policy: final transaction has ${operations.length} operations, expected 1`);
290
+ }
291
+ const op = operations[0];
261
292
  const hostFn = op.body().invokeHostFunctionOp()?.hostFunction();
262
- if (!hostFn || hostFn.switch().name !== 'hostFunctionTypeInvokeContract') {
293
+ if (hostFn?.switch().name !== 'hostFunctionTypeInvokeContract') {
263
294
  throw new Error('install_policy: built op is not an invokeHostFunction(invokeContract(...)) call');
264
295
  }
265
296
  const invokeArgs = hostFn.invokeContract();
@@ -412,10 +443,6 @@ function decodeInstallCallDescribes(tx, op, expectedInstallNonce) {
412
443
  // `observedInstallNonce` is the nonce baked into whichever interpreter
413
444
  // policy is present; when none, fall back to the caller-supplied value.
414
445
  const installNonce = observedInstallNonce ?? expectedInstallNonce;
415
- // Touch `tx` so the linter does not flag it as unused - the parameter
416
- // documents that the caller built this Transaction; we re-parse the
417
- // operation off it as a sanity check.
418
- void tx;
419
446
  return {
420
447
  targetContract,
421
448
  fnName: 'add_context_rule',
@@ -0,0 +1,96 @@
1
+ import { Contract } from '@stellar/stellar-sdk';
2
+ import type { InstallRpcClient } from './build-install-policy.ts';
3
+ /** Inputs for the install-predicate build. */
4
+ export interface BuildInstallPredicateArgs {
5
+ /** The smart account contract address (C...). The interpreter hashes
6
+ * this rule's `signers` at install; we fetch it via `get_context_rule`
7
+ * on this address, not reconstruct it. */
8
+ smartAccount: string;
9
+ /** The signer that authorises the install (G... wallet). */
10
+ sourceAccount: string;
11
+ /** The rule id the account assigned in call 1. */
12
+ ruleId: number;
13
+ /** The interpreter contract address (C...). */
14
+ interpreterAddress: string;
15
+ /** Network passphrase - pins the hash the auth digest binds. */
16
+ networkPassphrase: string;
17
+ /** Already-encoded (base64) canonical ScVal of the predicate. */
18
+ encodedPredicate: string;
19
+ /** Hex sha256 of the canonical predicate XDR bytes. */
20
+ predicateHash: string;
21
+ /** Grammar version override; defaults to 1. */
22
+ grammarVersion?: number;
23
+ /** Base fee in stroops; defaults to BASE_FEE (100). */
24
+ baseFee?: number;
25
+ /** RPC client to use for the simulation pass; injected for tests. */
26
+ rpc: InstallRpcClient;
27
+ /** Ledger window (in ledgers) for the auth entry's `validUntil`. */
28
+ authValidUntilLedgers?: number;
29
+ }
30
+ /** Human-readable description of the install-predicate call, decoded FROM
31
+ * the built XDR (not from the input args). The wallet signature binds
32
+ * to bytes; the review card has to bind to the same bytes, so this is
33
+ * the only safe source.
34
+ *
35
+ * `ruleId` is NOT decoded from the XDR - it lives inside
36
+ * `raw_context_rule`, which is passed through verbatim. Decoding it
37
+ * here would re-introduce the client-side reconstruction the file
38
+ * header warns against. The top-level `BuildInstallPredicateResult`
39
+ * echoes the caller-supplied rule id instead. */
40
+ export interface InstallPredicateCallDescribes {
41
+ /** The interpreter that will hold the predicate. */
42
+ targetContract: string;
43
+ /** The fn name on the target (always `install` today). */
44
+ fnName: 'install';
45
+ /** Grammar version the interpreter enforces; read off the wire. */
46
+ grammarVersion: number;
47
+ /** Per-rule install nonce; read off the wire. */
48
+ installNonce: number;
49
+ /** Hex sha256 of the encoded predicate blob; read off the wire. */
50
+ predicateHash: string;
51
+ /** Hex sha256 of the predicate bytes embedded in the params map. The
52
+ * two values MUST match; a difference is a wire tampering signal. */
53
+ predicateSha256OfEmbeddedBytes: string;
54
+ /** The smart account address encoded in args[2], read off the wire. */
55
+ smartAccountOnWire: string;
56
+ }
57
+ /** Output of the install-predicate build. The unsigned XDR is the
58
+ * wallet's input; the wallet signs it (single envelope signature) and
59
+ * submits. The auth tree matches install_policy: account entry
60
+ * carrying OZ's AuthPayload + a source-account delegated signer entry.
61
+ * Both are covered by the envelope signature. */
62
+ export interface BuildInstallPredicateResult {
63
+ /** Unsigned Soroban transaction envelope, base64 XDR. */
64
+ unsignedXdr: string;
65
+ /** Smart account contract address (echo). */
66
+ smartAccount: string;
67
+ /** Source account (echo) - the address that signs the envelope. */
68
+ sourceAccount: string;
69
+ /** The interpreter contract address (echo). */
70
+ interpreterAddress: string;
71
+ /** The rule id the account assigned in call 1 (echo). */
72
+ ruleId: number;
73
+ /** The host-call target + fn name. */
74
+ call: {
75
+ contract: string;
76
+ fn: 'install';
77
+ };
78
+ /** The smart-account auth nonce (snapshot). */
79
+ authNonce: string;
80
+ /** The ledger sequence + window the auth entry expires at. */
81
+ authValidUntilLedger: number;
82
+ /** The captured rootInvocation for the smart-account entry. */
83
+ rootInvocationXdr: string;
84
+ /** Human-readable description of the install call, decoded FROM the
85
+ * built unsigned XDR (not from the input args). The wallet signature
86
+ * binds to bytes; the review card has to bind to the same bytes, so
87
+ * this is the only safe source. */
88
+ describes: InstallPredicateCallDescribes;
89
+ }
90
+ /** Build the unsigned transaction envelope for
91
+ * `interpreter.install(params, raw_context_rule, smart_account)`. The
92
+ * output XDR is signed by the wallet, not by us. */
93
+ export declare function buildInstallPredicateXdr(args: BuildInstallPredicateArgs): Promise<BuildInstallPredicateResult>;
94
+ /** Re-export `Contract` so the run-layer does not need to import the
95
+ * SDK directly. Mirrors the build-install-policy.ts convention. */
96
+ export { Contract };