@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.
- package/README.md +4 -4
- package/dist/adapters/interpreter/adapter.d.ts +19 -25
- package/dist/adapters/interpreter/adapter.js +54 -487
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/install/build-add-context-rule.d.ts +3 -19
- package/dist/install/build-add-context-rule.js +3 -98
- package/dist/install/build-install-policy.d.ts +4 -53
- package/dist/install/build-install-policy.js +44 -123
- package/dist/install/index.d.ts +0 -2
- package/dist/install/index.js +0 -7
- package/dist/predicate/decode.d.ts +1 -1
- package/dist/predicate/decode.js +2 -70
- package/dist/predicate/encode.js +45 -229
- package/dist/predicate/from-json.js +1 -42
- package/dist/record/decode.js +3 -8
- package/dist/record/freshness.d.ts +17 -11
- package/dist/record/freshness.js +28 -18
- package/dist/review-card/builder.d.ts +4 -3
- package/dist/review-card/builder.js +18 -162
- package/dist/review-card/cross-check.js +2 -105
- package/dist/review-card/render-leaf.d.ts +4 -0
- package/dist/review-card/render-leaf.js +47 -0
- package/dist/run/index.d.ts +36 -139
- package/dist/run/index.js +125 -437
- package/dist/run/schemas.d.ts +168 -1929
- package/dist/run/schemas.js +40 -269
- package/dist/{synth → simulate}/deny-cases.d.ts +4 -8
- package/dist/simulate/deny-cases.js +282 -0
- package/dist/simulate/evaluate.d.ts +18 -0
- package/dist/simulate/evaluate.js +271 -0
- package/dist/simulate/index.d.ts +4 -0
- package/dist/simulate/index.js +8 -0
- package/dist/synth/compose-from-recording.d.ts +24 -51
- package/dist/synth/compose-from-recording.js +120 -282
- package/dist/synth/index.d.ts +0 -5
- package/dist/synth/index.js +0 -5
- package/dist/synth/synthesize-from-recording.d.ts +1 -29
- package/dist/synth/synthesize-from-recording.js +41 -356
- package/dist/types.d.ts +8 -60
- package/dist/types.js +5 -2
- package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
- package/dist-cjs/adapters/interpreter/adapter.js +56 -489
- package/dist-cjs/errors.d.ts +1 -1
- package/dist-cjs/index.d.ts +1 -4
- package/dist-cjs/index.js +1 -4
- package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
- package/dist-cjs/install/build-add-context-rule.js +1 -97
- package/dist-cjs/install/build-install-policy.d.ts +4 -53
- package/dist-cjs/install/build-install-policy.js +44 -122
- package/dist-cjs/install/index.d.ts +0 -2
- package/dist-cjs/install/index.js +2 -23
- package/dist-cjs/predicate/decode.d.ts +1 -1
- package/dist-cjs/predicate/decode.js +2 -70
- package/dist-cjs/predicate/encode.js +45 -229
- package/dist-cjs/predicate/from-json.js +1 -42
- package/dist-cjs/record/decode.js +3 -8
- package/dist-cjs/record/freshness.d.ts +17 -11
- package/dist-cjs/record/freshness.js +28 -18
- package/dist-cjs/review-card/builder.d.ts +4 -3
- package/dist-cjs/review-card/builder.js +23 -167
- package/dist-cjs/review-card/cross-check.js +7 -110
- package/dist-cjs/review-card/render-leaf.d.ts +4 -0
- package/dist-cjs/review-card/render-leaf.js +52 -0
- package/dist-cjs/run/index.d.ts +36 -139
- package/dist-cjs/run/index.js +125 -444
- package/dist-cjs/run/schemas.d.ts +168 -1929
- package/dist-cjs/run/schemas.js +41 -270
- package/dist-cjs/{synth → simulate}/deny-cases.d.ts +4 -8
- package/dist-cjs/simulate/deny-cases.js +286 -0
- package/dist-cjs/simulate/evaluate.d.ts +18 -0
- package/dist-cjs/simulate/evaluate.js +274 -0
- package/dist-cjs/simulate/index.d.ts +4 -0
- package/dist-cjs/simulate/index.js +13 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
- package/dist-cjs/synth/compose-from-recording.js +120 -282
- package/dist-cjs/synth/index.d.ts +0 -5
- package/dist-cjs/synth/index.js +1 -11
- package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
- package/dist-cjs/synth/synthesize-from-recording.js +39 -354
- package/dist-cjs/types.d.ts +8 -60
- package/dist-cjs/types.js +6 -3
- package/package.json +5 -5
- package/src/adapters/interpreter/adapter.ts +76 -572
- package/src/errors.ts +0 -19
- package/src/index.ts +1 -4
- package/src/install/build-add-context-rule.ts +5 -139
- package/src/install/build-install-policy.ts +93 -214
- package/src/install/index.ts +0 -34
- package/src/predicate/decode.ts +2 -70
- package/src/predicate/encode.ts +49 -261
- package/src/predicate/from-json.ts +1 -43
- package/src/record/decode.ts +3 -8
- package/src/record/freshness.ts +29 -18
- package/src/review-card/builder.ts +19 -168
- package/src/review-card/cross-check.ts +3 -105
- package/src/review-card/render-leaf.ts +48 -0
- package/src/run/index.ts +144 -572
- package/src/run/schemas.ts +42 -291
- package/src/simulate/deny-cases.ts +334 -0
- package/src/simulate/evaluate.ts +284 -0
- package/src/simulate/index.ts +11 -0
- package/src/synth/compose-from-recording.ts +148 -347
- package/src/synth/index.ts +0 -13
- package/src/synth/synthesize-from-recording.ts +43 -456
- package/src/types.ts +13 -49
- package/dist/adapters/interpreter/index.d.ts +0 -1
- package/dist/adapters/interpreter/index.js +0 -2
- package/dist/adapters/oz/adapter.d.ts +0 -20
- package/dist/adapters/oz/adapter.js +0 -295
- package/dist/adapters/oz/index.d.ts +0 -1
- package/dist/adapters/oz/index.js +0 -2
- package/dist/codegen/compile-gate.d.ts +0 -33
- package/dist/codegen/compile-gate.js +0 -124
- package/dist/codegen/index.d.ts +0 -2
- package/dist/codegen/index.js +0 -8
- package/dist/codegen/template.d.ts +0 -18
- package/dist/codegen/template.js +0 -148
- package/dist/install/authority-overlap.d.ts +0 -134
- package/dist/install/authority-overlap.js +0 -0
- package/dist/install/build-install-predicate.d.ts +0 -96
- package/dist/install/build-install-predicate.js +0 -444
- package/dist/install/build-merge-policy.d.ts +0 -70
- package/dist/install/build-merge-policy.js +0 -130
- package/dist/install/plan-merge-policy.d.ts +0 -49
- package/dist/install/plan-merge-policy.js +0 -86
- package/dist/install/read-account-rules.d.ts +0 -100
- package/dist/install/read-account-rules.js +0 -283
- package/dist/ir/index.d.ts +0 -1
- package/dist/ir/index.js +0 -2
- package/dist/ir/types.d.ts +0 -140
- package/dist/ir/types.js +0 -11
- package/dist/mandate/index.d.ts +0 -2
- package/dist/mandate/index.js +0 -2
- package/dist/mandate/to-ir.d.ts +0 -3
- package/dist/mandate/to-ir.js +0 -60
- package/dist/mandate/types.d.ts +0 -20
- package/dist/mandate/types.js +0 -8
- package/dist/seams/index.d.ts +0 -1
- package/dist/seams/index.js +0 -2
- package/dist/seams/types.d.ts +0 -66
- package/dist/seams/types.js +0 -11
- package/dist/synth/deny-cases.js +0 -562
- package/dist/synth/evaluate.d.ts +0 -39
- package/dist/synth/evaluate.js +0 -551
- package/dist/synth/harness.d.ts +0 -28
- package/dist/synth/harness.js +0 -47
- package/dist/synth/minimize.d.ts +0 -4
- package/dist/synth/minimize.js +0 -38
- package/dist/synth/permit-context.d.ts +0 -15
- package/dist/synth/permit-context.js +0 -116
- package/dist/synth/predicate-literals.d.ts +0 -5
- package/dist/synth/predicate-literals.js +0 -25
- package/dist/synth/synthesize-from-mandate.d.ts +0 -20
- package/dist/synth/synthesize-from-mandate.js +0 -59
- package/dist/verify/envelope.d.ts +0 -15
- package/dist/verify/envelope.js +0 -22
- package/dist/verify/index.d.ts +0 -3
- package/dist/verify/index.js +0 -3
- package/dist/verify/simulate.d.ts +0 -31
- package/dist/verify/simulate.js +0 -258
- package/dist/verify/verify.d.ts +0 -21
- package/dist/verify/verify.js +0 -189
- package/dist-cjs/adapters/interpreter/index.d.ts +0 -1
- package/dist-cjs/adapters/interpreter/index.js +0 -8
- package/dist-cjs/adapters/oz/adapter.d.ts +0 -20
- package/dist-cjs/adapters/oz/adapter.js +0 -300
- package/dist-cjs/adapters/oz/index.d.ts +0 -1
- package/dist-cjs/adapters/oz/index.js +0 -8
- package/dist-cjs/codegen/compile-gate.d.ts +0 -33
- package/dist-cjs/codegen/compile-gate.js +0 -128
- package/dist-cjs/codegen/index.d.ts +0 -2
- package/dist-cjs/codegen/index.js +0 -14
- package/dist-cjs/codegen/template.d.ts +0 -18
- package/dist-cjs/codegen/template.js +0 -151
- package/dist-cjs/install/authority-overlap.d.ts +0 -134
- package/dist-cjs/install/authority-overlap.js +0 -0
- package/dist-cjs/install/build-install-predicate.d.ts +0 -96
- package/dist-cjs/install/build-install-predicate.js +0 -479
- package/dist-cjs/install/build-merge-policy.d.ts +0 -70
- package/dist-cjs/install/build-merge-policy.js +0 -134
- package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
- package/dist-cjs/install/plan-merge-policy.js +0 -90
- package/dist-cjs/install/read-account-rules.d.ts +0 -100
- package/dist-cjs/install/read-account-rules.js +0 -296
- package/dist-cjs/ir/index.d.ts +0 -1
- package/dist-cjs/ir/index.js +0 -3
- package/dist-cjs/ir/types.d.ts +0 -140
- package/dist-cjs/ir/types.js +0 -12
- package/dist-cjs/mandate/index.d.ts +0 -2
- package/dist-cjs/mandate/index.js +0 -6
- package/dist-cjs/mandate/to-ir.d.ts +0 -3
- package/dist-cjs/mandate/to-ir.js +0 -63
- package/dist-cjs/mandate/types.d.ts +0 -20
- package/dist-cjs/mandate/types.js +0 -9
- package/dist-cjs/seams/index.d.ts +0 -1
- package/dist-cjs/seams/index.js +0 -3
- package/dist-cjs/seams/types.d.ts +0 -66
- package/dist-cjs/seams/types.js +0 -12
- package/dist-cjs/synth/deny-cases.js +0 -568
- package/dist-cjs/synth/evaluate.d.ts +0 -39
- package/dist-cjs/synth/evaluate.js +0 -554
- package/dist-cjs/synth/harness.d.ts +0 -28
- package/dist-cjs/synth/harness.js +0 -50
- package/dist-cjs/synth/minimize.d.ts +0 -4
- package/dist-cjs/synth/minimize.js +0 -41
- package/dist-cjs/synth/permit-context.d.ts +0 -15
- package/dist-cjs/synth/permit-context.js +0 -119
- package/dist-cjs/synth/predicate-literals.d.ts +0 -5
- package/dist-cjs/synth/predicate-literals.js +0 -28
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +0 -20
- package/dist-cjs/synth/synthesize-from-mandate.js +0 -62
- package/dist-cjs/verify/envelope.d.ts +0 -15
- package/dist-cjs/verify/envelope.js +0 -23
- package/dist-cjs/verify/index.d.ts +0 -3
- package/dist-cjs/verify/index.js +0 -8
- package/dist-cjs/verify/simulate.d.ts +0 -31
- package/dist-cjs/verify/simulate.js +0 -261
- package/dist-cjs/verify/verify.d.ts +0 -21
- package/dist-cjs/verify/verify.js +0 -192
- package/src/adapters/interpreter/index.ts +0 -8
- package/src/adapters/oz/adapter.ts +0 -376
- package/src/adapters/oz/index.ts +0 -9
- package/src/codegen/compile-gate.ts +0 -167
- package/src/codegen/index.ts +0 -17
- package/src/codegen/template.ts +0 -165
- package/src/install/authority-overlap.ts +0 -0
- package/src/install/build-merge-policy.ts +0 -219
- package/src/install/plan-merge-policy.ts +0 -133
- package/src/install/read-account-rules.ts +0 -376
- package/src/ir/index.ts +0 -13
- package/src/ir/types.ts +0 -132
- package/src/mandate/index.ts +0 -4
- package/src/mandate/to-ir.ts +0 -71
- package/src/mandate/types.ts +0 -21
- package/src/seams/index.ts +0 -11
- package/src/seams/types.ts +0 -81
- package/src/synth/deny-cases.ts +0 -663
- package/src/synth/evaluate.ts +0 -613
- package/src/synth/harness.ts +0 -68
- package/src/synth/minimize.ts +0 -48
- package/src/synth/permit-context.ts +0 -136
- package/src/synth/predicate-literals.ts +0 -27
- package/src/synth/synthesize-from-mandate.ts +0 -82
- package/src/verify/envelope.ts +0 -28
- package/src/verify/index.ts +0 -5
- package/src/verify/simulate.ts +0 -311
- package/src/verify/verify.ts +0 -243
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { createHash } from 'node:crypto'
|
|
21
21
|
import {
|
|
22
|
+
Account,
|
|
22
23
|
Address,
|
|
23
24
|
BASE_FEE,
|
|
24
25
|
Contract,
|
|
@@ -30,7 +31,8 @@ import {
|
|
|
30
31
|
TransactionBuilder,
|
|
31
32
|
xdr,
|
|
32
33
|
} from '@stellar/stellar-sdk'
|
|
33
|
-
import {
|
|
34
|
+
import type { ContextRuleDraft } from '../types.ts'
|
|
35
|
+
import { buildAddContextRuleArgs } from './build-add-context-rule.ts'
|
|
34
36
|
import {
|
|
35
37
|
accountEntry,
|
|
36
38
|
authDigest,
|
|
@@ -106,20 +108,14 @@ export interface BuildInstallPolicyArgs {
|
|
|
106
108
|
sourceAccount: string
|
|
107
109
|
/** Network passphrase - pins the hash the auth digest binds. */
|
|
108
110
|
networkPassphrase: string
|
|
109
|
-
/** The new rule to install.
|
|
110
|
-
rule:
|
|
111
|
+
/** The new rule to install. */
|
|
112
|
+
rule: ContextRuleDraft
|
|
111
113
|
/** Per-rule install nonce; 1 for a fresh install. */
|
|
112
114
|
installNonce: number
|
|
113
115
|
/** Already-encoded (base64) canonical ScVal of the predicate. */
|
|
114
116
|
encodedPredicate: string
|
|
115
117
|
/** Hex sha256 of the canonical predicate XDR bytes. */
|
|
116
118
|
predicateHash: string
|
|
117
|
-
/** Per-policy oracle overrides. */
|
|
118
|
-
oracleParams?: {
|
|
119
|
-
maxStalenessSeconds?: number
|
|
120
|
-
maxDeviationBps?: number
|
|
121
|
-
maxCrossFeedDeviationBps?: number
|
|
122
|
-
}
|
|
123
119
|
/** Grammar version override; defaults to 1. */
|
|
124
120
|
grammarVersion?: number
|
|
125
121
|
/** Base fee in stroops; defaults to BASE_FEE (100). */
|
|
@@ -130,36 +126,6 @@ export interface BuildInstallPolicyArgs {
|
|
|
130
126
|
authValidUntilLedgers?: number
|
|
131
127
|
}
|
|
132
128
|
|
|
133
|
-
export type BuildInstallPolicyRuleDraft = {
|
|
134
|
-
contextRuleType:
|
|
135
|
-
| { kind: 'default' }
|
|
136
|
-
| { kind: 'call_contract'; contract: string }
|
|
137
|
-
| { kind: 'create_contract'; wasmHash: string }
|
|
138
|
-
name: string
|
|
139
|
-
validUntilLedger: number | null
|
|
140
|
-
signers: BuildInstallPolicySignerDraft[]
|
|
141
|
-
policies: BuildInstallPolicyPolicyRef[]
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export type BuildInstallPolicySignerDraft =
|
|
145
|
-
| { kind: 'delegated'; address: string }
|
|
146
|
-
| { kind: 'external'; verifier: string; keyBytes: string }
|
|
147
|
-
|
|
148
|
-
export type BuildInstallPolicyPolicyRef =
|
|
149
|
-
| {
|
|
150
|
-
kind: 'interpreter'
|
|
151
|
-
interpreterAddress: string
|
|
152
|
-
predicateBlobBase64: string
|
|
153
|
-
}
|
|
154
|
-
| {
|
|
155
|
-
kind: 'oz_builtin'
|
|
156
|
-
instanceAddress: string
|
|
157
|
-
primitive: {
|
|
158
|
-
primitive: 'spending_limit' | 'simple_threshold' | 'weighted_threshold'
|
|
159
|
-
params: Record<string, unknown>
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
129
|
/** Human-readable description of the install call, decoded FROM the built
|
|
164
130
|
* XDR (not from the input args). The XDR is the source of truth: a
|
|
165
131
|
* human approving a review card needs the description to mirror the bytes
|
|
@@ -193,20 +159,13 @@ export interface InstallCallDescribes {
|
|
|
193
159
|
* sha256 of the predicate blob actually embedded in the XDR - so a
|
|
194
160
|
* mismatch between the wire bytes and the review card is detectable
|
|
195
161
|
* by reading `describes`. */
|
|
196
|
-
policies: Array<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
| {
|
|
205
|
-
kind: 'oz_builtin'
|
|
206
|
-
address: string
|
|
207
|
-
primitive: 'spending_limit' | 'simple_threshold' | 'weighted_threshold'
|
|
208
|
-
}
|
|
209
|
-
>
|
|
162
|
+
policies: Array<{
|
|
163
|
+
kind: 'interpreter'
|
|
164
|
+
address: string
|
|
165
|
+
installNonce: number
|
|
166
|
+
predicateHash: string
|
|
167
|
+
predicateSha256OfEmbeddedBytes: string
|
|
168
|
+
}>
|
|
210
169
|
/** The install nonce, decoded from the interpreter policy's
|
|
211
170
|
* `install_nonce` field. Echoed at the top level for reviewer convenience;
|
|
212
171
|
* the per-policy entry is the source of truth. */
|
|
@@ -261,102 +220,30 @@ export async function buildInstallPolicyXdr(
|
|
|
261
220
|
name: args.rule.name,
|
|
262
221
|
validUntilLedger: args.rule.validUntilLedger,
|
|
263
222
|
signers: args.rule.signers,
|
|
264
|
-
policies: args.rule.policies
|
|
223
|
+
policies: args.rule.policies,
|
|
265
224
|
},
|
|
266
225
|
{
|
|
267
226
|
signers: args.rule.signers,
|
|
268
|
-
policies: args.rule.policies
|
|
227
|
+
policies: args.rule.policies,
|
|
269
228
|
installNonce: args.installNonce,
|
|
270
229
|
encodedPredicate: args.encodedPredicate,
|
|
271
230
|
predicateHash: args.predicateHash,
|
|
272
|
-
...(args.oracleParams ? { oracleParams: args.oracleParams } : {}),
|
|
273
231
|
...(args.grammarVersion !== undefined ? { grammarVersion: args.grammarVersion } : {}),
|
|
274
232
|
}
|
|
275
233
|
)
|
|
276
234
|
|
|
277
|
-
// 2.
|
|
278
|
-
// bare host call. The recording pass assigns the smart-account auth nonce
|
|
279
|
-
// and root invocation needed to construct OZ's AuthPayload.
|
|
280
|
-
const source = await args.rpc.getAccount(args.sourceAccount)
|
|
281
|
-
const hostFunction = xdr.HostFunction.hostFunctionTypeInvokeContract(
|
|
282
|
-
new xdr.InvokeContractArgs({
|
|
283
|
-
contractAddress: new Address(args.smartAccount).toScAddress(),
|
|
284
|
-
functionName: 'add_context_rule',
|
|
285
|
-
args: [...callArgs],
|
|
286
|
-
})
|
|
287
|
-
)
|
|
288
|
-
const makeOperation = (auth: xdr.SorobanAuthorizationEntry[] = []) =>
|
|
289
|
-
Operation.invokeHostFunction({ func: hostFunction, auth })
|
|
290
|
-
const baseFee = args.baseFee !== undefined ? String(args.baseFee) : BASE_FEE
|
|
291
|
-
const buildTx = (op: xdr.Operation) =>
|
|
292
|
-
buildUnsignedTx({
|
|
293
|
-
sourceAccount: args.sourceAccount,
|
|
294
|
-
sequence: source.sequenceNumber(),
|
|
295
|
-
fee: baseFee,
|
|
296
|
-
networkPassphrase: args.networkPassphrase,
|
|
297
|
-
op,
|
|
298
|
-
})
|
|
299
|
-
const recordingTx = buildTx(makeOperation())
|
|
300
|
-
|
|
301
|
-
// 3. Recording simulation: capture the smart account's nonce and invocation
|
|
302
|
-
// tree. Nothing is signed here.
|
|
303
|
-
const recorded = await args.rpc.simulateTransaction(recordingTx)
|
|
304
|
-
if (rpc.Api.isSimulationError(recorded)) {
|
|
305
|
-
// Short, stable reason. The full `simulateTransaction` error (which
|
|
306
|
-
// carries host + URL detail) stays in the SDK's own logs - never
|
|
307
|
-
// reflected back into a user-facing message where it would
|
|
308
|
-
// reconnoitre the RPC.
|
|
309
|
-
throw new Error('install_policy: simulateTransaction failed')
|
|
310
|
-
}
|
|
311
|
-
const original = (recorded.result?.auth ?? []).find(
|
|
312
|
-
(entry) =>
|
|
313
|
-
entry.credentials().switch().name === 'sorobanCredentialsAddress' &&
|
|
314
|
-
Address.fromScAddress(entry.credentials().address().address()).toString() ===
|
|
315
|
-
args.smartAccount
|
|
316
|
-
)
|
|
317
|
-
if (!original) {
|
|
318
|
-
throw new Error(
|
|
319
|
-
`install_policy: no Soroban auth entry for smart account ${args.smartAccount}; this call does not route through the smart account`
|
|
320
|
-
)
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// 4. `add_context_rule` is authorised by the deploy-time admin rule (rule 0).
|
|
235
|
+
// 2. `add_context_rule` is authorised by the deploy-time admin rule (rule 0).
|
|
324
236
|
// The delegated signer uses source-account credentials, so its signature
|
|
325
237
|
// bytes stay empty and the ordinary transaction-envelope signature covers
|
|
326
238
|
// the call when the consumer signs it.
|
|
327
|
-
const validUntilLedger =
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
signaturePayload(
|
|
333
|
-
args.networkPassphrase,
|
|
334
|
-
original.credentials().address().nonce(),
|
|
335
|
-
validUntilLedger,
|
|
336
|
-
original.rootInvocation()
|
|
337
|
-
),
|
|
338
|
-
contextRuleIds
|
|
239
|
+
const { finalTx, original, validUntilLedger } = await buildAuthorisedSmartAccountTx(
|
|
240
|
+
args,
|
|
241
|
+
'add_context_rule',
|
|
242
|
+
[...callArgs],
|
|
243
|
+
'install_policy'
|
|
339
244
|
)
|
|
340
|
-
const authEntries = [
|
|
341
|
-
accountEntry(
|
|
342
|
-
original,
|
|
343
|
-
validUntilLedger,
|
|
344
|
-
authPayload([args.sourceAccount], contextRuleIds, () => Buffer.alloc(0))
|
|
345
|
-
),
|
|
346
|
-
...contextRuleIds.map(() => delegatedSignerEntry(args.smartAccount, digest)),
|
|
347
|
-
]
|
|
348
245
|
|
|
349
|
-
//
|
|
350
|
-
// existing auth during assembly and adds the simulated Soroban footprint +
|
|
351
|
-
// resource fee, yielding a complete unsigned envelope.
|
|
352
|
-
const txWithAuth = buildTx(makeOperation(authEntries))
|
|
353
|
-
const enforcing = await args.rpc.simulateTransaction(txWithAuth)
|
|
354
|
-
if (rpc.Api.isSimulationError(enforcing)) {
|
|
355
|
-
throw new Error('install_policy: auth simulateTransaction failed')
|
|
356
|
-
}
|
|
357
|
-
const finalTx = rpc.assembleTransaction(txWithAuth, enforcing).build()
|
|
358
|
-
|
|
359
|
-
// 6. Decode the structured description FROM the final assembled transaction.
|
|
246
|
+
// 3. Decode the structured description FROM the final assembled transaction.
|
|
360
247
|
// The human approval binds to the exact bytes the wallet will sign.
|
|
361
248
|
const describes = decodeInstallCallDescribes(finalTx, args.installNonce)
|
|
362
249
|
|
|
@@ -392,12 +279,75 @@ export async function buildRevokePolicyXdr(args: {
|
|
|
392
279
|
baseFee?: number
|
|
393
280
|
authValidUntilLedgers?: number
|
|
394
281
|
}): Promise<BuildRevokePolicyResult> {
|
|
282
|
+
// Removal is master-only, so the deploy-time admin rule authorises it. The
|
|
283
|
+
// recorded rootInvocation still includes remove_context_rule(ruleId), binding
|
|
284
|
+
// the auth payload to the exact rule being removed. As with install,
|
|
285
|
+
// source-account credentials carry the delegated signer entry and the
|
|
286
|
+
// consumer supplies only the ordinary envelope signature.
|
|
287
|
+
const { finalTx, original, validUntilLedger } = await buildAuthorisedSmartAccountTx(
|
|
288
|
+
args,
|
|
289
|
+
'remove_context_rule',
|
|
290
|
+
[xdr.ScVal.scvU32(args.ruleId)],
|
|
291
|
+
'revoke_policy'
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
unsignedXdr: finalTx.toEnvelope().toXDR().toString('base64'),
|
|
296
|
+
smartAccount: args.smartAccount,
|
|
297
|
+
sourceAccount: args.sourceAccount,
|
|
298
|
+
call: { contract: args.smartAccount, fn: 'remove_context_rule', ruleId: args.ruleId },
|
|
299
|
+
authNonce: original.credentials().address().nonce().toString(),
|
|
300
|
+
authValidUntilLedger: validUntilLedger,
|
|
301
|
+
rootInvocationXdr: original.rootInvocation().toXDR().toString('base64'),
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface BuildRevokePolicyResult {
|
|
306
|
+
unsignedXdr: string
|
|
307
|
+
smartAccount: string
|
|
308
|
+
sourceAccount: string
|
|
309
|
+
call: { contract: string; fn: 'remove_context_rule'; ruleId: number }
|
|
310
|
+
authNonce: string
|
|
311
|
+
authValidUntilLedger: number
|
|
312
|
+
rootInvocationXdr: string
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** ~25 minutes at 5s/ledger. */
|
|
316
|
+
const DEFAULT_AUTH_VALID_UNTIL_LEDGERS = 300
|
|
317
|
+
|
|
318
|
+
// ---- internals ----
|
|
319
|
+
|
|
320
|
+
/** Record a bare call to the smart account, attach the deploy-time admin rule's
|
|
321
|
+
* auth entries, and re-simulate to assemble the footprint.
|
|
322
|
+
*
|
|
323
|
+
* Install and revoke differ only in the call they wrap, so they share this
|
|
324
|
+
* spine: the recording pass supplies the auth nonce and root invocation OZ's
|
|
325
|
+
* AuthPayload binds to, and the second simulation adds the Soroban footprint +
|
|
326
|
+
* resource fee. Nothing here is signed. `errorPrefix` names the calling tool in
|
|
327
|
+
* the fail-closed messages so a caller can still tell which call failed. */
|
|
328
|
+
async function buildAuthorisedSmartAccountTx(
|
|
329
|
+
args: {
|
|
330
|
+
smartAccount: string
|
|
331
|
+
sourceAccount: string
|
|
332
|
+
networkPassphrase: string
|
|
333
|
+
rpc: InstallRpcClient
|
|
334
|
+
baseFee?: number
|
|
335
|
+
authValidUntilLedgers?: number
|
|
336
|
+
},
|
|
337
|
+
functionName: string,
|
|
338
|
+
callArgs: xdr.ScVal[],
|
|
339
|
+
errorPrefix: string
|
|
340
|
+
): Promise<{
|
|
341
|
+
finalTx: Transaction
|
|
342
|
+
original: xdr.SorobanAuthorizationEntry
|
|
343
|
+
validUntilLedger: number
|
|
344
|
+
}> {
|
|
395
345
|
const source = await args.rpc.getAccount(args.sourceAccount)
|
|
396
346
|
const hostFunction = xdr.HostFunction.hostFunctionTypeInvokeContract(
|
|
397
347
|
new xdr.InvokeContractArgs({
|
|
398
348
|
contractAddress: new Address(args.smartAccount).toScAddress(),
|
|
399
|
-
functionName
|
|
400
|
-
args:
|
|
349
|
+
functionName,
|
|
350
|
+
args: callArgs,
|
|
401
351
|
})
|
|
402
352
|
)
|
|
403
353
|
const makeOperation = (auth: xdr.SorobanAuthorizationEntry[] = []) =>
|
|
@@ -418,7 +368,7 @@ export async function buildRevokePolicyXdr(args: {
|
|
|
418
368
|
// carries host + URL detail) stays in the SDK's own logs - never
|
|
419
369
|
// reflected back into a user-facing message where it would
|
|
420
370
|
// reconnoitre the RPC.
|
|
421
|
-
throw new Error(
|
|
371
|
+
throw new Error(`${errorPrefix}: simulateTransaction failed`)
|
|
422
372
|
}
|
|
423
373
|
const original = (recorded.result?.auth ?? []).find(
|
|
424
374
|
(entry) =>
|
|
@@ -428,15 +378,10 @@ export async function buildRevokePolicyXdr(args: {
|
|
|
428
378
|
)
|
|
429
379
|
if (!original) {
|
|
430
380
|
throw new Error(
|
|
431
|
-
|
|
381
|
+
`${errorPrefix}: no Soroban auth entry for smart account ${args.smartAccount}; this call does not route through the smart account`
|
|
432
382
|
)
|
|
433
383
|
}
|
|
434
384
|
|
|
435
|
-
// Removal is master-only, so the deploy-time admin rule authorises it. The
|
|
436
|
-
// recorded rootInvocation still includes remove_context_rule(ruleId), binding
|
|
437
|
-
// the auth payload to the exact rule being removed. As with install,
|
|
438
|
-
// source-account credentials carry the delegated signer entry and the
|
|
439
|
-
// consumer supplies only the ordinary envelope signature.
|
|
440
385
|
const validUntilLedger =
|
|
441
386
|
(await args.rpc.getLatestLedger()).sequence +
|
|
442
387
|
(args.authValidUntilLedgers ?? DEFAULT_AUTH_VALID_UNTIL_LEDGERS)
|
|
@@ -461,52 +406,12 @@ export async function buildRevokePolicyXdr(args: {
|
|
|
461
406
|
const txWithAuth = buildTx(makeOperation(authEntries))
|
|
462
407
|
const enforcing = await args.rpc.simulateTransaction(txWithAuth)
|
|
463
408
|
if (rpc.Api.isSimulationError(enforcing)) {
|
|
464
|
-
throw new Error(
|
|
465
|
-
}
|
|
466
|
-
const finalTx = rpc.assembleTransaction(txWithAuth, enforcing).build()
|
|
467
|
-
|
|
468
|
-
return {
|
|
469
|
-
unsignedXdr: finalTx.toEnvelope().toXDR().toString('base64'),
|
|
470
|
-
smartAccount: args.smartAccount,
|
|
471
|
-
sourceAccount: args.sourceAccount,
|
|
472
|
-
call: { contract: args.smartAccount, fn: 'remove_context_rule', ruleId: args.ruleId },
|
|
473
|
-
authNonce: original.credentials().address().nonce().toString(),
|
|
474
|
-
authValidUntilLedger: validUntilLedger,
|
|
475
|
-
rootInvocationXdr: original.rootInvocation().toXDR().toString('base64'),
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
export interface BuildRevokePolicyResult {
|
|
480
|
-
unsignedXdr: string
|
|
481
|
-
smartAccount: string
|
|
482
|
-
sourceAccount: string
|
|
483
|
-
call: { contract: string; fn: 'remove_context_rule'; ruleId: number }
|
|
484
|
-
authNonce: string
|
|
485
|
-
authValidUntilLedger: number
|
|
486
|
-
rootInvocationXdr: string
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/** ~25 minutes at 5s/ledger. */
|
|
490
|
-
const DEFAULT_AUTH_VALID_UNTIL_LEDGERS = 300
|
|
491
|
-
|
|
492
|
-
// ---- internals ----
|
|
493
|
-
|
|
494
|
-
/** Adapter: turn the install-policy wire `PolicyRef` shape into the core
|
|
495
|
-
* `PolicyRef` shape `buildAddContextRuleArgs` expects. Keeps the wire
|
|
496
|
-
* schema hand-rolled + flat (the strict union would need a recursive
|
|
497
|
-
* schema) while delegating to the proven encoder for the actual bytes. */
|
|
498
|
-
function adaptPolicyRef(p: BuildInstallPolicyPolicyRef) {
|
|
499
|
-
if (p.kind === 'interpreter') {
|
|
500
|
-
return {
|
|
501
|
-
kind: 'interpreter' as const,
|
|
502
|
-
interpreterAddress: p.interpreterAddress,
|
|
503
|
-
predicateBlobBase64: p.predicateBlobBase64,
|
|
504
|
-
}
|
|
409
|
+
throw new Error(`${errorPrefix}: auth simulateTransaction failed`)
|
|
505
410
|
}
|
|
506
411
|
return {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
412
|
+
finalTx: rpc.assembleTransaction(txWithAuth, enforcing).build(),
|
|
413
|
+
original,
|
|
414
|
+
validUntilLedger,
|
|
510
415
|
}
|
|
511
416
|
}
|
|
512
417
|
|
|
@@ -580,14 +485,13 @@ function decodeInstallCallDescribes(
|
|
|
580
485
|
`install_policy: built op has ${scvArgs.length} args, expected 5 (context_type, name, valid_until, signers, policies)`
|
|
581
486
|
)
|
|
582
487
|
}
|
|
583
|
-
const contextType = scvArgs[0]
|
|
584
488
|
const nameScv = scvArgs[1]
|
|
585
489
|
const validUntilScv = scvArgs[2]
|
|
586
490
|
const signersScv = scvArgs[3]
|
|
587
491
|
const policiesScv = scvArgs[4]
|
|
588
492
|
// The length check above pins these as defined; the local references
|
|
589
493
|
// satisfy noUncheckedIndexedAccess on the accessors below.
|
|
590
|
-
if (!nameScv || !validUntilScv || !signersScv || !policiesScv
|
|
494
|
+
if (!nameScv || !validUntilScv || !signersScv || !policiesScv) {
|
|
591
495
|
throw new Error('install_policy: built op args include an undefined slot despite length check')
|
|
592
496
|
}
|
|
593
497
|
|
|
@@ -672,7 +576,6 @@ function decodeInstallCallDescribes(
|
|
|
672
576
|
fields.set(fk.sym().toString(), inner.val())
|
|
673
577
|
}
|
|
674
578
|
// Interpreter policy fields carry grammar_version/install_nonce/predicate.
|
|
675
|
-
// OZ primitives carry spending_limit/period_ledgers/threshold/signers.
|
|
676
579
|
if (fields.has('predicate') || fields.has('grammar_version') || fields.has('install_nonce')) {
|
|
677
580
|
const installNonceScv = fields.get('install_nonce')
|
|
678
581
|
if (installNonceScv?.switch().name !== 'scvU32') {
|
|
@@ -706,21 +609,6 @@ function decodeInstallCallDescribes(
|
|
|
706
609
|
observedInstallNonce = installNonce
|
|
707
610
|
continue
|
|
708
611
|
}
|
|
709
|
-
// OZ built-in primitive. Distinguish by the parameter shape we
|
|
710
|
-
// emitted; matching one of the three primitives exactly pins the
|
|
711
|
-
// kind we built.
|
|
712
|
-
if (fields.has('spending_limit') && fields.has('period_ledgers')) {
|
|
713
|
-
policies.push({ kind: 'oz_builtin', address, primitive: 'spending_limit' })
|
|
714
|
-
continue
|
|
715
|
-
}
|
|
716
|
-
if (fields.has('threshold') && fields.has('signer_weights')) {
|
|
717
|
-
policies.push({ kind: 'oz_builtin', address, primitive: 'weighted_threshold' })
|
|
718
|
-
continue
|
|
719
|
-
}
|
|
720
|
-
if (fields.has('threshold')) {
|
|
721
|
-
policies.push({ kind: 'oz_builtin', address, primitive: 'simple_threshold' })
|
|
722
|
-
continue
|
|
723
|
-
}
|
|
724
612
|
throw new Error(
|
|
725
613
|
`install_policy: policies[${address}] value has an unknown field set; the encoder may have drifted`
|
|
726
614
|
)
|
|
@@ -752,12 +640,3 @@ function decodeInstallCallDescribes(
|
|
|
752
640
|
installNonce,
|
|
753
641
|
}
|
|
754
642
|
}
|
|
755
|
-
|
|
756
|
-
/** Re-export so the run-layer does not need to import from
|
|
757
|
-
* build-add-context-rule.ts (keeps the install/ -> install/ dependency
|
|
758
|
-
* direction intact). */
|
|
759
|
-
export { Contract, DEFAULT_GRAMMAR_VERSION }
|
|
760
|
-
|
|
761
|
-
// Local re-import to avoid pulling the class from the SDK module path
|
|
762
|
-
// at the top of the file (avoids the unused-import lint).
|
|
763
|
-
import { Account } from '@stellar/stellar-sdk'
|
package/src/install/index.ts
CHANGED
|
@@ -12,29 +12,6 @@
|
|
|
12
12
|
// Exported here rather than from the package root to keep the root surface
|
|
13
13
|
// about synthesis, and because these are transaction-building primitives whose
|
|
14
14
|
// callers should know they are reaching for them.
|
|
15
|
-
|
|
16
|
-
// Cross-rule authority analysis. A caller installing a policy needs to know
|
|
17
|
-
// whether its signers can already reach the same calls through another context
|
|
18
|
-
// rule, because OZ lets the signer name the rule and enforces only that one's
|
|
19
|
-
// policies. Without this, a second, tighter rule reads as a restriction while
|
|
20
|
-
// restricting nothing.
|
|
21
|
-
export {
|
|
22
|
-
ANY,
|
|
23
|
-
type AuthorityOverlap,
|
|
24
|
-
type ContextType,
|
|
25
|
-
effectiveSelectors,
|
|
26
|
-
findAuthorityOverlaps,
|
|
27
|
-
type IntendedInstall,
|
|
28
|
-
intersectSelectors,
|
|
29
|
-
type MergeResult,
|
|
30
|
-
mergeIntoAnd,
|
|
31
|
-
type ObservedRule,
|
|
32
|
-
type OverlapSeverity,
|
|
33
|
-
permittedSelectors,
|
|
34
|
-
type RuleClass,
|
|
35
|
-
type Selector,
|
|
36
|
-
signerKey,
|
|
37
|
-
} from './authority-overlap.ts'
|
|
38
15
|
export {
|
|
39
16
|
ADD_CONTEXT_RULE_SYMBOL,
|
|
40
17
|
type AddContextRuleArgs,
|
|
@@ -42,14 +19,3 @@ export {
|
|
|
42
19
|
buildAddContextRuleArgs,
|
|
43
20
|
DEFAULT_GRAMMAR_VERSION,
|
|
44
21
|
} from './build-add-context-rule.ts'
|
|
45
|
-
export {
|
|
46
|
-
type AccountRuleReader,
|
|
47
|
-
accountRuleReaderFromServer,
|
|
48
|
-
type CollectedRules,
|
|
49
|
-
collectObservedRules,
|
|
50
|
-
decodeContextRule,
|
|
51
|
-
decodeContextType,
|
|
52
|
-
decodeSigner,
|
|
53
|
-
docLedgerKey,
|
|
54
|
-
MAX_RULE_ID_SCAN,
|
|
55
|
-
} from './read-account-rules.ts'
|
package/src/predicate/decode.ts
CHANGED
|
@@ -64,21 +64,6 @@ function expectSymbol(v: xdr.ScVal | undefined, what: string): string {
|
|
|
64
64
|
return v.sym().toString()
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function expectAddress(v: xdr.ScVal | undefined, what: string): string {
|
|
68
|
-
if (!v || v.switch() !== xdr.ScValType.scvAddress()) throw malformed(`${what} is not an address`)
|
|
69
|
-
return Address.fromScAddress(v.address()).toString()
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function expectU64(v: xdr.ScVal | undefined, what: string): string {
|
|
73
|
-
if (!v || v.switch() !== xdr.ScValType.scvU64()) throw malformed(`${what} is not a u64`)
|
|
74
|
-
return v.u64().toString()
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function expectI128(v: xdr.ScVal | undefined, what: string): string {
|
|
78
|
-
if (!v || v.switch() !== xdr.ScValType.scvI128()) throw malformed(`${what} is not an i128`)
|
|
79
|
-
return scValToBigInt(v).toString()
|
|
80
|
-
}
|
|
81
|
-
|
|
82
67
|
/** Arity check with the same intent as the Rust `check_arity`: a selector with
|
|
83
68
|
* the wrong element count is malformed, not silently truncated. */
|
|
84
69
|
function arity(items: xdr.ScVal[], n: number, selector: string): void {
|
|
@@ -109,48 +94,6 @@ function decodeSelectorLeaf(items: xdr.ScVal[], sym: string): PredicateLeaf {
|
|
|
109
94
|
element: expectU32(items[2], 'call_arg_field element'),
|
|
110
95
|
field: expectSymbol(items[3], 'call_arg_field field'),
|
|
111
96
|
}
|
|
112
|
-
case 'call_arg_scaled':
|
|
113
|
-
arity(items, 4, sym)
|
|
114
|
-
return {
|
|
115
|
-
kind: 'call_arg_scaled',
|
|
116
|
-
index: expectU32(items[1], 'call_arg_scaled index'),
|
|
117
|
-
num: expectI128(items[2], 'call_arg_scaled num'),
|
|
118
|
-
den: expectI128(items[3], 'call_arg_scaled den'),
|
|
119
|
-
}
|
|
120
|
-
case 'amount':
|
|
121
|
-
arity(items, 2, sym)
|
|
122
|
-
return { kind: 'amount', token: expectAddress(items[1], 'amount token') }
|
|
123
|
-
case 'window_spent':
|
|
124
|
-
arity(items, 3, sym)
|
|
125
|
-
return {
|
|
126
|
-
kind: 'window_spent',
|
|
127
|
-
token: expectAddress(items[1], 'window_spent token'),
|
|
128
|
-
windowSeconds: Number(expectU64(items[2], 'window_spent windowSeconds')),
|
|
129
|
-
}
|
|
130
|
-
case 'now':
|
|
131
|
-
arity(items, 1, sym)
|
|
132
|
-
return { kind: 'now' }
|
|
133
|
-
case 'valid_until':
|
|
134
|
-
// The encoder refuses to emit this and the interpreter refuses it at
|
|
135
|
-
// install, so it cannot appear in a document that installed. Decoding it
|
|
136
|
-
// anyway would present a rule the chain would never have accepted.
|
|
137
|
-
throw malformed('valid_until is not a usable predicate leaf')
|
|
138
|
-
case 'invocation_count':
|
|
139
|
-
arity(items, 2, sym)
|
|
140
|
-
return {
|
|
141
|
-
kind: 'invocation_count_in_window',
|
|
142
|
-
windowSecs: Number(expectU64(items[1], 'invocation_count windowSecs')),
|
|
143
|
-
}
|
|
144
|
-
case 'oracle_price':
|
|
145
|
-
arity(items, 2, sym)
|
|
146
|
-
return { kind: 'oracle_price', asset: expectAddress(items[1], 'oracle_price asset') }
|
|
147
|
-
case 'oracle_threshold':
|
|
148
|
-
arity(items, 3, sym)
|
|
149
|
-
return {
|
|
150
|
-
kind: 'oracle_threshold',
|
|
151
|
-
value: expectI128(items[1], 'oracle_threshold value'),
|
|
152
|
-
decimals: expectU32(items[2], 'oracle_threshold decimals'),
|
|
153
|
-
}
|
|
154
97
|
default:
|
|
155
98
|
// Deliberately NOT a literal_vec fallback - see the header note.
|
|
156
99
|
throw malformed(`unknown selector symbol '${sym}'`)
|
|
@@ -173,38 +116,27 @@ export function decodeLeaf(v: xdr.ScVal): PredicateLeaf {
|
|
|
173
116
|
return { kind: 'literal_symbol', value: v.sym().toString() }
|
|
174
117
|
case xdr.ScValType.scvU32():
|
|
175
118
|
return { kind: 'literal_u32', value: v.u32() }
|
|
176
|
-
case xdr.ScValType.scvU64():
|
|
177
|
-
return { kind: 'literal_u64', value: v.u64().toString() }
|
|
178
119
|
case xdr.ScValType.scvI128():
|
|
179
120
|
return { kind: 'literal_i128', value: scValToBigInt(v).toString() }
|
|
180
|
-
case xdr.ScValType.scvBytes():
|
|
181
|
-
return { kind: 'literal_bytes', value: Buffer.from(v.bytes()).toString('hex') }
|
|
182
121
|
default:
|
|
183
122
|
throw malformed(`unsupported leaf value type ${v.switch().name}`)
|
|
184
123
|
}
|
|
185
124
|
}
|
|
186
125
|
|
|
187
|
-
/** One node.
|
|
126
|
+
/** One node. The comparison ops wrap two LEAVES. */
|
|
188
127
|
export function decodeNode(v: xdr.ScVal): PredicateNode {
|
|
189
128
|
const items = expectVec(v, 'node')
|
|
190
129
|
const op = selectorSymbol(items)
|
|
191
130
|
if (op === null) throw malformed('node does not start with an operator symbol')
|
|
192
131
|
switch (op) {
|
|
193
|
-
case 'and':
|
|
194
|
-
case 'or': {
|
|
132
|
+
case 'and': {
|
|
195
133
|
arity(items, 2, op)
|
|
196
134
|
const children = expectVec(items[1] as xdr.ScVal, `${op} children`).map(decodeNode)
|
|
197
135
|
if (children.length === 0) throw malformed(`${op} has no children`)
|
|
198
136
|
return { op, children }
|
|
199
137
|
}
|
|
200
|
-
case 'not':
|
|
201
|
-
arity(items, 2, op)
|
|
202
|
-
return { op: 'not', child: decodeNode(items[1] as xdr.ScVal) }
|
|
203
138
|
case 'eq':
|
|
204
|
-
case 'lt':
|
|
205
139
|
case 'lte':
|
|
206
|
-
case 'gt':
|
|
207
|
-
case 'gte':
|
|
208
140
|
arity(items, 3, op)
|
|
209
141
|
return {
|
|
210
142
|
op,
|