@crediolabs/policy-synth 0.1.4 → 0.1.5

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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/dist/codegen/template.js +3 -3
  3. package/dist/record/decode.d.ts +6 -3
  4. package/dist/record/decode.js +13 -3
  5. package/dist/run/index.d.ts +31 -0
  6. package/dist/run/index.js +164 -0
  7. package/dist/run/schemas.d.ts +2003 -0
  8. package/dist/run/schemas.js +215 -0
  9. package/dist/synth/deny-cases.js +18 -2
  10. package/dist/synth/evaluate.js +2 -2
  11. package/dist/synth/synthesize-from-recording.d.ts +5 -2
  12. package/dist/synth/synthesize-from-recording.js +121 -63
  13. package/dist/types.d.ts +17 -0
  14. package/dist/types.js +17 -0
  15. package/dist/verify/simulate.js +18 -2
  16. package/dist/verify/verify.js +18 -2
  17. package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
  18. package/dist-cjs/adapters/interpreter/adapter.js +527 -0
  19. package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
  20. package/dist-cjs/adapters/interpreter/index.js +8 -0
  21. package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
  22. package/dist-cjs/adapters/oz/adapter.js +289 -0
  23. package/dist-cjs/adapters/oz/index.d.ts +1 -0
  24. package/dist-cjs/adapters/oz/index.js +8 -0
  25. package/dist-cjs/codegen/compile-gate.d.ts +33 -0
  26. package/dist-cjs/codegen/compile-gate.js +123 -0
  27. package/dist-cjs/codegen/index.d.ts +2 -0
  28. package/dist-cjs/codegen/index.js +14 -0
  29. package/dist-cjs/codegen/template.d.ts +18 -0
  30. package/dist-cjs/codegen/template.js +134 -0
  31. package/dist-cjs/errors.d.ts +37 -0
  32. package/dist-cjs/errors.js +3 -0
  33. package/dist-cjs/index.d.ts +11 -0
  34. package/dist-cjs/index.js +27 -0
  35. package/dist-cjs/ir/index.d.ts +1 -0
  36. package/dist-cjs/ir/index.js +3 -0
  37. package/dist-cjs/ir/types.d.ts +108 -0
  38. package/dist-cjs/ir/types.js +12 -0
  39. package/dist-cjs/mandate/index.d.ts +2 -0
  40. package/dist-cjs/mandate/index.js +6 -0
  41. package/dist-cjs/mandate/to-ir.d.ts +3 -0
  42. package/dist-cjs/mandate/to-ir.js +63 -0
  43. package/dist-cjs/mandate/types.d.ts +20 -0
  44. package/dist-cjs/mandate/types.js +9 -0
  45. package/dist-cjs/package.json +3 -0
  46. package/dist-cjs/predicate/encode.d.ts +10 -0
  47. package/dist-cjs/predicate/encode.js +252 -0
  48. package/dist-cjs/predicate/index.d.ts +1 -0
  49. package/dist-cjs/predicate/index.js +6 -0
  50. package/dist-cjs/record/decode.d.ts +79 -0
  51. package/dist-cjs/record/decode.js +398 -0
  52. package/dist-cjs/record/freshness.d.ts +17 -0
  53. package/dist-cjs/record/freshness.js +55 -0
  54. package/dist-cjs/record/index.d.ts +21 -0
  55. package/dist-cjs/record/index.js +166 -0
  56. package/dist-cjs/record/movements.d.ts +20 -0
  57. package/dist-cjs/record/movements.js +192 -0
  58. package/dist-cjs/record/rpc.d.ts +22 -0
  59. package/dist-cjs/record/rpc.js +74 -0
  60. package/dist-cjs/record/validate.d.ts +22 -0
  61. package/dist-cjs/record/validate.js +63 -0
  62. package/dist-cjs/registry/identify.d.ts +11 -0
  63. package/dist-cjs/registry/identify.js +87 -0
  64. package/dist-cjs/registry/index.d.ts +3 -0
  65. package/dist-cjs/registry/index.js +15 -0
  66. package/dist-cjs/registry/known-addresses.d.ts +16 -0
  67. package/dist-cjs/registry/known-addresses.js +53 -0
  68. package/dist-cjs/registry/protocols.d.ts +38 -0
  69. package/dist-cjs/registry/protocols.js +153 -0
  70. package/dist-cjs/review-card/builder.d.ts +14 -0
  71. package/dist-cjs/review-card/builder.js +264 -0
  72. package/dist-cjs/review-card/conflict.d.ts +40 -0
  73. package/dist-cjs/review-card/conflict.js +114 -0
  74. package/dist-cjs/review-card/cross-check.d.ts +11 -0
  75. package/dist-cjs/review-card/cross-check.js +151 -0
  76. package/dist-cjs/review-card/index.d.ts +3 -0
  77. package/dist-cjs/review-card/index.js +10 -0
  78. package/dist-cjs/run/index.d.ts +31 -0
  79. package/dist-cjs/run/index.js +178 -0
  80. package/dist-cjs/run/schemas.d.ts +2003 -0
  81. package/dist-cjs/run/schemas.js +218 -0
  82. package/dist-cjs/seams/index.d.ts +1 -0
  83. package/dist-cjs/seams/index.js +3 -0
  84. package/dist-cjs/seams/types.d.ts +66 -0
  85. package/dist-cjs/seams/types.js +12 -0
  86. package/dist-cjs/synth/compose-from-recording.d.ts +64 -0
  87. package/dist-cjs/synth/compose-from-recording.js +356 -0
  88. package/dist-cjs/synth/deny-cases.d.ts +12 -0
  89. package/dist-cjs/synth/deny-cases.js +380 -0
  90. package/dist-cjs/synth/evaluate.d.ts +39 -0
  91. package/dist-cjs/synth/evaluate.js +428 -0
  92. package/dist-cjs/synth/harness.d.ts +16 -0
  93. package/dist-cjs/synth/harness.js +29 -0
  94. package/dist-cjs/synth/index.d.ts +9 -0
  95. package/dist-cjs/synth/index.js +23 -0
  96. package/dist-cjs/synth/lower.d.ts +23 -0
  97. package/dist-cjs/synth/lower.js +119 -0
  98. package/dist-cjs/synth/minimize.d.ts +4 -0
  99. package/dist-cjs/synth/minimize.js +41 -0
  100. package/dist-cjs/synth/predicate-literals.d.ts +5 -0
  101. package/dist-cjs/synth/predicate-literals.js +28 -0
  102. package/dist-cjs/synth/scope.d.ts +26 -0
  103. package/dist-cjs/synth/scope.js +81 -0
  104. package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
  105. package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
  106. package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
  107. package/dist-cjs/synth/synthesize-from-recording.js +711 -0
  108. package/dist-cjs/types.d.ts +288 -0
  109. package/dist-cjs/types.js +55 -0
  110. package/dist-cjs/verify/envelope.d.ts +15 -0
  111. package/dist-cjs/verify/envelope.js +23 -0
  112. package/dist-cjs/verify/index.d.ts +3 -0
  113. package/dist-cjs/verify/index.js +8 -0
  114. package/dist-cjs/verify/simulate.d.ts +31 -0
  115. package/dist-cjs/verify/simulate.js +261 -0
  116. package/dist-cjs/verify/verify.d.ts +21 -0
  117. package/dist-cjs/verify/verify.js +192 -0
  118. package/package.json +43 -3
  119. package/src/codegen/template.ts +3 -3
  120. package/src/record/decode.ts +21 -10
  121. package/src/run/index.ts +213 -0
  122. package/src/run/schemas.ts +242 -0
  123. package/src/synth/deny-cases.ts +21 -2
  124. package/src/synth/evaluate.ts +2 -2
  125. package/src/synth/synthesize-from-recording.ts +141 -62
  126. package/src/types.ts +19 -0
  127. package/src/verify/simulate.ts +21 -2
  128. package/src/verify/verify.ts +21 -2
@@ -0,0 +1,398 @@
1
+ "use strict";
2
+ // src/record/decode.ts - pure XDR -> invocation tree decoder.
3
+ //
4
+ // Inputs: a base64-encoded TransactionEnvelope XDR (or one already decoded),
5
+ // the raw transaction events (used only to identify contract emitters), and
6
+ // (optionally) the auth-entry set.
7
+ //
8
+ // Outputs:
9
+ // - top-level ContractInvocation (the single Context `Policy::enforce` receives)
10
+ // - subInvocations captured on each ContractInvocation from
11
+ // SorobanAuthorizedInvocation.subInvocations - DIAGNOSTIC ONLY;
12
+ // v1 grammar does NOT walk sub-invocations (see types.ts notes).
13
+ // - decoded args vector mapped to the normalised `ScVal` subset
14
+ // - sourceAccount, signers
15
+ // - raw event list surfaced for downstream validation
16
+ // - auth entries surfaced for downstream validation
17
+ //
18
+ // No network calls. No randomness. No globals.
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.DecodeError = void 0;
21
+ exports.decodeEnvelopeXdr = decodeEnvelopeXdr;
22
+ exports.decodeEnvelope = decodeEnvelope;
23
+ exports.decodeScAddressToAnyStrkey = decodeScAddressToAnyStrkey;
24
+ exports.decodeScAddressToC = decodeScAddressToC;
25
+ exports.scValToSubset = scValToSubset;
26
+ exports.i128PartsToBigInt = i128PartsToBigInt;
27
+ exports.u128PartsToBigInt = u128PartsToBigInt;
28
+ exports.u64PartsToBigInt = u64PartsToBigInt;
29
+ exports.scValToTopicString = scValToTopicString;
30
+ exports.contractEventsToOnChainEvents = contractEventsToOnChainEvents;
31
+ exports.transactionEventsToOnChainEvents = transactionEventsToOnChainEvents;
32
+ exports.tokenMovementKey = tokenMovementKey;
33
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
34
+ const identify_ts_1 = require("../registry/identify.js");
35
+ const types_ts_1 = require("../types.js");
36
+ /** Decode a TransactionEnvelope XDR (base64) into a fully parsed invocation tree. */
37
+ function decodeEnvelopeXdr(envelopeXdrB64, events = [], _authEntries = [], ledgerSequence = 0, knownContracts = new Set(), network = null) {
38
+ const envelope = stellar_sdk_1.xdr.TransactionEnvelope.fromXDR(envelopeXdrB64, 'base64');
39
+ return decodeEnvelope(envelope, events, _authEntries, ledgerSequence, knownContracts, network);
40
+ }
41
+ /** Same as `decodeEnvelopeXdr` but accepts an already-decoded envelope. Useful
42
+ * for tests that build the envelope via SDK helpers (no XDR round-trip). */
43
+ function decodeEnvelope(envelope, events = [], _authEntries = [], ledgerSequence = 0, knownContracts = new Set(), network = null) {
44
+ const envType = envelope.switch().name;
45
+ if (envType === 'envelopeTypeTxFeeBump') {
46
+ // Fee-bump wraps a normal inner v1 transaction (a different account
47
+ // pays the fee). The real operations + their authorizers live on the
48
+ // INNER v1 envelope, so decode that and discard the outer fee-bump
49
+ // shell. The fee-bump envelope switch arm `v0` is intentionally not
50
+ // routed here - fee-bump v0 does not exist.
51
+ const innerV1 = envelope.feeBump().tx().innerTx().v1();
52
+ return decodeV1Envelope(innerV1, events, ledgerSequence, knownContracts, network);
53
+ }
54
+ if (envType !== 'envelopeTypeTx') {
55
+ // TransactionV0 is out of scope - v1 protocol only. Fee-bump is now
56
+ // handled above; legacy envelopes reach this branch.
57
+ throw new DecodeError(`unsupported envelope: ${envType}`);
58
+ }
59
+ return decodeV1Envelope(envelope.v1(), events, ledgerSequence, knownContracts, network);
60
+ }
61
+ /** Decode a v1 TransactionEnvelope (either a top-level v1 envelope or the
62
+ * inner v1 envelope of a fee-bump wrapper). Source, operations, auth
63
+ * entries, signers all come from this v1 envelope. */
64
+ function decodeV1Envelope(v1, events, ledgerSequence, knownContracts, network) {
65
+ const tx = v1.tx();
66
+ const sourceAccount = decodeMuxedOrEd25519(tx.sourceAccount());
67
+ const signers = extractSigners(v1, tx);
68
+ const invocations = [];
69
+ const projectedAuthEntries = [];
70
+ const opaqueScVals = [];
71
+ const knownContractSet = new Set();
72
+ const unknownContracts = [];
73
+ const knownContractsOut = [];
74
+ for (const op of tx.operations()) {
75
+ const body = op.body();
76
+ if (body.switch().name !== 'invokeHostFunction')
77
+ continue;
78
+ const invokeOp = body.invokeHostFunctionOp();
79
+ const auth = invokeOp.auth();
80
+ projectedAuthEntries.push(...auth.map(projectAuthEntry));
81
+ const hostFn = invokeOp.hostFunction();
82
+ if (hostFn.switch().name !== 'hostFunctionTypeInvokeContract')
83
+ continue;
84
+ const invokeArgs = hostFn.invokeContract();
85
+ const contract = decodeScAddressToC(invokeArgs.contractAddress());
86
+ const fn = symbolValueFromBufferOrString(invokeArgs.functionName());
87
+ const argsScval = invokeArgs.args();
88
+ const args = [];
89
+ argsScval.forEach((a, i) => {
90
+ args.push(scValToSubset(a, `args[${i}]`, opaqueScVals));
91
+ });
92
+ const subInvocations = decodeSubInvocations(auth, opaqueScVals);
93
+ invocations.push({ contract, fn, args, subInvocations });
94
+ recordInvocation(contract, fn, args, network, knownContracts, knownContractSet, knownContractsOut, unknownContracts);
95
+ }
96
+ return {
97
+ sourceAccount,
98
+ signers,
99
+ invocations,
100
+ events,
101
+ authEntries: projectedAuthEntries,
102
+ opaqueScVals,
103
+ unknownContracts,
104
+ knownContracts: knownContractsOut,
105
+ ledgerSequence,
106
+ };
107
+ }
108
+ /** Extract the union of signers from:
109
+ * - envelope-level signatures (the tx-level signers), and
110
+ * - Soroban auth entries that are `SorobanCredentials.sorobanCredentialsAddress`
111
+ * (the contract-Caller authorised via ed25519/address credentials).
112
+ * Returns G... strkeys (or address strkeys for non-account credentials). */
113
+ function extractSigners(v1, tx) {
114
+ const set = new Set();
115
+ for (const sig of v1.signatures()) {
116
+ // Each signature has a 4-byte hint. We cannot recover the full strkey from
117
+ // a 4-byte hint alone - so we record it as `hint:<hex>` to preserve signal
118
+ // for the downstream reviewer without claiming a match we cannot prove.
119
+ const hintBytes = sig.hint();
120
+ set.add(`hint:${Buffer.from(hintBytes).toString('hex')}`);
121
+ }
122
+ // Soroban address credentials - recover the strkey address.
123
+ for (const op of tx.operations()) {
124
+ const body = op.body();
125
+ if (body.switch().name !== 'invokeHostFunction')
126
+ continue;
127
+ const invokeOp = body.invokeHostFunctionOp();
128
+ for (const entry of invokeOp.auth()) {
129
+ const creds = entry.credentials();
130
+ const kind = creds.switch().name;
131
+ if (kind === 'sorobanCredentialsAddress') {
132
+ const addrCreds = creds.address();
133
+ const scAddr = addrCreds.address();
134
+ try {
135
+ const strkey = decodeScAddressToAnyStrkey(scAddr);
136
+ if (strkey)
137
+ set.add(strkey);
138
+ }
139
+ catch {
140
+ // ignore - we record it as opaque via freshness
141
+ }
142
+ }
143
+ // sorobanCredentialsSourceAccount -> pulled from tx.sourceAccount() above
144
+ }
145
+ }
146
+ return Array.from(set);
147
+ }
148
+ const MAX_AUTH_TREE_DEPTH = 16;
149
+ function projectAuthEntry(entry) {
150
+ const credentials = entry.credentials();
151
+ const authorizingAddress = credentials.switch().name === 'sorobanCredentialsAddress'
152
+ ? decodeScAddressToAnyStrkey(credentials.address().address())
153
+ : null;
154
+ const fn = entry.rootInvocation().function();
155
+ if (fn.switch().name === 'sorobanAuthorizedFunctionTypeContractFn') {
156
+ const contractFn = fn.contractFn();
157
+ return {
158
+ authorizingAddress,
159
+ contract: decodeScAddressToC(contractFn.contractAddress()),
160
+ fn: symbolValueFromBufferOrString(contractFn.functionName()),
161
+ };
162
+ }
163
+ return { authorizingAddress, contract: '', fn: '__create_contract__' };
164
+ }
165
+ function decodeSubInvocations(authEntries, opaqueScVals) {
166
+ return authEntries.flatMap((entry, index) => {
167
+ const decoded = decodeAuthorizedInvocation(entry.rootInvocation(), opaqueScVals, `auth[${index}].root`, 0);
168
+ return decoded ? [decoded] : [];
169
+ });
170
+ }
171
+ function decodeAuthorizedInvocation(inv, opaqueScVals, path, depth) {
172
+ if (depth >= MAX_AUTH_TREE_DEPTH) {
173
+ opaqueScVals.push({ path, type: 'auth-tree-depth-exceeded' });
174
+ return null;
175
+ }
176
+ const fn = inv.function();
177
+ if (fn.switch().name !== 'sorobanAuthorizedFunctionTypeContractFn')
178
+ return null;
179
+ const contractFn = fn.contractFn();
180
+ const contract = decodeScAddressToC(contractFn.contractAddress());
181
+ const fnName = symbolValueFromBufferOrString(contractFn.functionName());
182
+ const args = contractFn
183
+ .args()
184
+ .map((arg, index) => scValToSubset(arg, `${path}.args[${index}]`, opaqueScVals));
185
+ const subInvocations = inv.subInvocations().flatMap((child, index) => {
186
+ const decoded = decodeAuthorizedInvocation(child, opaqueScVals, `${path}.subInvocations[${index}]`, depth + 1);
187
+ return decoded ? [decoded] : [];
188
+ });
189
+ return { contract, fn: fnName, args, subInvocations };
190
+ }
191
+ /** Decode an ScAddress (either account or contract) to its strkey form. */
192
+ function decodeScAddressToAnyStrkey(scAddr) {
193
+ const kind = scAddr.switch().name;
194
+ if (kind === 'scAddressTypeAccount') {
195
+ return decodePublicKeyToStrkey(scAddr.accountId());
196
+ }
197
+ if (kind === 'scAddressTypeContract') {
198
+ const hashBytes = scAddr.contractId();
199
+ return stellar_sdk_1.Address.contract(Buffer.from(hashBytes)).toString();
200
+ }
201
+ return null;
202
+ }
203
+ /** Decode an ScAddress to a C... contract strkey. */
204
+ function decodeScAddressToC(scAddr) {
205
+ if (scAddr.switch().name === 'scAddressTypeContract') {
206
+ const hashBytes = scAddr.contractId();
207
+ return stellar_sdk_1.Address.contract(Buffer.from(hashBytes)).toString();
208
+ }
209
+ if (scAddr.switch().name === 'scAddressTypeAccount') {
210
+ const strkey = decodePublicKeyToStrkey(scAddr.accountId());
211
+ if (strkey)
212
+ return strkey;
213
+ }
214
+ throw new DecodeError(`cannot convert ScAddress ${scAddr.switch().name} to contract strkey`);
215
+ }
216
+ /** Decode a tx source account (MuxedEd25519Account OR AccountId OR PublicKey).
217
+ * - muxedAccount -> underlying ed25519 account as G...
218
+ * - publicKeyTypeEd25519 -> G...
219
+ */
220
+ function decodeMuxedOrEd25519(src) {
221
+ const kind = src.switch().name;
222
+ if (kind === 'keyTypeMuxedEd25519') {
223
+ const ed = src.med25519().ed25519();
224
+ return stellar_sdk_1.StrKey.encodeEd25519PublicKey(ed);
225
+ }
226
+ const ed = src.ed25519();
227
+ return stellar_sdk_1.StrKey.encodeEd25519PublicKey(ed);
228
+ }
229
+ /** AccountId is a type alias for PublicKey in this SDK; extract the ed25519
230
+ * strkey from a PublicKey union. */
231
+ function decodePublicKeyToStrkey(pubKey) {
232
+ if (pubKey.switch().name === 'publicKeyTypeEd25519') {
233
+ return stellar_sdk_1.StrKey.encodeEd25519PublicKey(pubKey.ed25519());
234
+ }
235
+ return null;
236
+ }
237
+ /** `functionName()` returns `string | Buffer` per the SDK type definition; we
238
+ * accept both and normalise. */
239
+ function symbolValueFromBufferOrString(s) {
240
+ return Buffer.isBuffer(s) ? s.toString('utf8') : s;
241
+ }
242
+ /** Map an arbitrary ScVal into the normalised `ScVal` subset defined in
243
+ * types.ts. Anything outside the subset is wrapped as `{type:'other', value}`
244
+ * AND recorded in `opaqueScVals` so the freshness module can reduce
245
+ * parseConfidence. Recursion is bounded by `MAX_SCVAL_DEPTH` so a
246
+ * hand-crafted nested-vec payload cannot RangeError the JS stack; the
247
+ * over-depth branch is collapsed to a single opaque `depth-exceeded` token
248
+ * (the recorder's parseConfidence gate then fails the recording closed). */
249
+ function scValToSubset(val, path, opaqueScVals, depth = 0) {
250
+ const kind = val.switch().name;
251
+ switch (kind) {
252
+ case 'scvAddress': {
253
+ const strkey = decodeScAddressToAnyStrkey(val.address());
254
+ if (strkey === null) {
255
+ opaqueScVals.push({ path, type: kind });
256
+ return { type: 'other', value: kind };
257
+ }
258
+ return { type: 'address', value: strkey };
259
+ }
260
+ case 'scvI128': {
261
+ const parts = val.i128();
262
+ return { type: 'i128', value: i128PartsToBigInt(parts).toString() };
263
+ }
264
+ case 'scvU64': {
265
+ return { type: 'u64', value: u64PartsToBigInt(val.u64()).toString() };
266
+ }
267
+ case 'scvU32': {
268
+ return { type: 'u32', value: val.u32().toString() };
269
+ }
270
+ case 'scvSymbol': {
271
+ return { type: 'symbol', value: val.sym().toString() };
272
+ }
273
+ case 'scvVec': {
274
+ if (depth >= types_ts_1.MAX_SCVAL_DEPTH) {
275
+ // Over-depth branch: collapse to a single opaque terminal so the
276
+ // downstream parseConfidence gate sees the fail-closed freshness hit.
277
+ opaqueScVals.push({ path, type: 'depth-exceeded' });
278
+ return { type: 'other', value: 'depth-exceeded' };
279
+ }
280
+ const arr = (val.vec() ?? []);
281
+ return {
282
+ type: 'vec',
283
+ value: arr.map((v, i) => scValToSubset(v, `${path}[${i}]`, opaqueScVals, depth + 1)),
284
+ };
285
+ }
286
+ case 'scvBytes': {
287
+ return { type: 'bytes', value: Buffer.from(val.bytes()).toString('hex') };
288
+ }
289
+ default: {
290
+ opaqueScVals.push({ path, type: kind });
291
+ return { type: 'other', value: kind };
292
+ }
293
+ }
294
+ }
295
+ /** Convert Int128Parts (hi: Int64, lo: Uint64) to a BigInt.
296
+ * hi is a signed 64-bit value; lo is unsigned. We use the SDK-provided
297
+ * `toString()` form which gives the signed decimal representation of the 64-bit
298
+ * Int64 directly. */
299
+ function i128PartsToBigInt(parts) {
300
+ const hiStr = parts.hi().toString();
301
+ const loStr = parts.lo().toString();
302
+ const hi = BigInt(hiStr);
303
+ const lo = BigInt(loStr);
304
+ return (hi << 64n) + lo;
305
+ }
306
+ /** Convert UInt128Parts (hi: Uint64, lo: Uint64) to an unsigned BigInt. */
307
+ function u128PartsToBigInt(parts) {
308
+ const hi = BigInt(parts.hi().toString());
309
+ const lo = BigInt(parts.lo().toString());
310
+ return (hi << 64n) + lo;
311
+ }
312
+ function u64PartsToBigInt(h) {
313
+ return BigInt(h.toString());
314
+ }
315
+ function recordInvocation(contract, fn, args, network, knownSet, knownContractSet, knownOut, unknownOut) {
316
+ // Recognition is evaluated PER INVOCATION using the (contract, fn, args)
317
+ // triple. The first call to a contract is no more authoritative than the
318
+ // second: an unrecognized call always contributes to the unknown count
319
+ // regardless of whether another invocation to the same contract was
320
+ // recognised. knownOut is deduplicated by contract (so the
321
+ // parseConfidence denominator isn't inflated by repeat calls), but each
322
+ // unrecognised invocation is recorded separately so the numerator
323
+ // reflects every fail-closed hit.
324
+ if (knownSet.has(contract)) {
325
+ if (!knownContractSet.has(contract)) {
326
+ knownContractSet.add(contract);
327
+ knownOut.push(contract);
328
+ }
329
+ return;
330
+ }
331
+ if ((0, identify_ts_1.identifyProtocol)(contract, fn, args, network ?? undefined)) {
332
+ if (!knownContractSet.has(contract)) {
333
+ knownContractSet.add(contract);
334
+ knownOut.push(contract);
335
+ }
336
+ return;
337
+ }
338
+ unknownOut.push({ contract, reason: 'no-abi' });
339
+ }
340
+ /** Convert a ScVal to a friendly string for use in OnChainEvent.topics
341
+ * (always-string scalar representation; vectors/bytes are stringified as JSON
342
+ * to keep the field string-typed). */
343
+ function scValToTopicString(val) {
344
+ const kind = val.switch().name;
345
+ if (kind === 'scvSymbol')
346
+ return val.sym().toString();
347
+ if (kind === 'scvString')
348
+ return val.str().toString();
349
+ if (kind === 'scvAddress') {
350
+ const strkey = decodeScAddressToAnyStrkey(val.address());
351
+ return strkey ?? `<unparsed-address:${kind}>`;
352
+ }
353
+ if (kind === 'scvU64')
354
+ return u64PartsToBigInt(val.u64()).toString();
355
+ if (kind === 'scvU32')
356
+ return val.u32().toString();
357
+ if (kind === 'scvI128')
358
+ return i128PartsToBigInt(val.i128()).toString();
359
+ if (kind === 'scvBytes')
360
+ return Buffer.from(val.bytes()).toString('hex');
361
+ return `<${kind}>`;
362
+ }
363
+ /** Build the OnChainEvent[] view from raw contract events. Diagnostic-only:
364
+ * captures the topics + data + emitter contract for the downstream
365
+ * validator. */
366
+ function contractEventsToOnChainEvents(contractEvents) {
367
+ const out = [];
368
+ for (const group of contractEvents) {
369
+ for (const evt of group) {
370
+ const body = evt.body().v0();
371
+ const emitter = evt.contractId()
372
+ ? stellar_sdk_1.Address.contract(Buffer.from(evt.contractId())).toString()
373
+ : '';
374
+ const topics = body.topics().map((t) => scValToTopicString(t));
375
+ const data = scValToSubset(body.data(), 'event.data', []);
376
+ out.push({ contract: emitter, topics, data });
377
+ }
378
+ }
379
+ return out;
380
+ }
381
+ /** Build the OnChainEvent[] view from TransactionEvent[] (which wraps
382
+ * ContractEvent with a stage). Same shape as above. */
383
+ function transactionEventsToOnChainEvents(txEvents) {
384
+ const contractEvents = txEvents.map((e) => [e.event()]);
385
+ return contractEventsToOnChainEvents(contractEvents);
386
+ }
387
+ /** Tiny typed error so callers can `instanceof DecodeError` without leaking
388
+ * SDK internals. */
389
+ class DecodeError extends Error {
390
+ name = 'DecodeError';
391
+ }
392
+ exports.DecodeError = DecodeError;
393
+ /** Helper used by validate.ts to map a TokenMovement back to a search key
394
+ * without re-parsing (the validate module must cross-check against the raw
395
+ * events, NOT against the parse). */
396
+ function tokenMovementKey(m) {
397
+ return `${m.token}|${m.from}|${m.to}|${m.amount}`;
398
+ }
@@ -0,0 +1,17 @@
1
+ import type { ParseConfidence } from '../types.ts';
2
+ /** Inputs to the freshness computation. The decode module produces the raw
3
+ * counters; this module applies the rule. */
4
+ export interface FreshnessInput {
5
+ knownContracts: string[];
6
+ unknownContracts: ParseConfidence['unknownContracts'];
7
+ opaqueScVals: ParseConfidence['opaqueScVals'];
8
+ }
9
+ /** Compute parseConfidence per the pinned rule. Pure: no IO. */
10
+ export declare function computeParseConfidence(input: FreshnessInput): ParseConfidence;
11
+ /** Refuse-on-low-confidence gate. Returns true when the recording must be
12
+ * refused (overall < thresholdUsed). The orchestrator wraps this in a
13
+ * ToolError with the ParseConfidence payload attached as `details`. */
14
+ export declare function isBelowThreshold(c: ParseConfidence): boolean;
15
+ /** Convenience: build the user-facing remediation question for an
16
+ * under-confidence recording. */
17
+ export declare function buildLowConfidenceQuestion(c: ParseConfidence): string;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ // src/record/freshness.ts - compute parseConfidence for a DecodedTransaction.
3
+ //
4
+ // The pinned rule (see ParseConfidence in src/types.ts):
5
+ // overall = 1 - (unknownContracts + opaqueScVals) / total
6
+ // where total = unique contracts referenced + opaque ScVal count.
7
+ // In v1 every contract is "unknown" by default (no adjacency ABI hint is
8
+ // bundled with the recorder); the freshness module still computes the value
9
+ // honestly so callers can see why the gate fires.
10
+ //
11
+ // thresholdUsed defaults to 1.0 - a stricter gate. Callers can opt to lower
12
+ // it via confidence_override but v1 ships only the default.
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.computeParseConfidence = computeParseConfidence;
15
+ exports.isBelowThreshold = isBelowThreshold;
16
+ exports.buildLowConfidenceQuestion = buildLowConfidenceQuestion;
17
+ /** Compute parseConfidence per the pinned rule. Pure: no IO. */
18
+ function computeParseConfidence(input) {
19
+ const unknown = input.unknownContracts.length;
20
+ const opaque = input.opaqueScVals.length;
21
+ const known = input.knownContracts.length;
22
+ // Pinned rule: overall = 1 - (unknown + opaque) / (known + unknown + opaque).
23
+ // Only the empty-decode case (nothing referenced) is guarded to avoid 0/0; it
24
+ // means "nothing to decode", so full confidence. A fully-unknown tx therefore
25
+ // scores 0, not an inflated value.
26
+ const denom = known + unknown + opaque;
27
+ const total = denom === 0 ? 1 : denom;
28
+ const overall = Math.max(0, Math.min(1, 1 - (unknown + opaque) / total));
29
+ return {
30
+ overall,
31
+ knownContracts: [...input.knownContracts],
32
+ unknownContracts: [...input.unknownContracts],
33
+ opaqueScVals: [...input.opaqueScVals],
34
+ thresholdUsed: 1.0,
35
+ };
36
+ }
37
+ /** Refuse-on-low-confidence gate. Returns true when the recording must be
38
+ * refused (overall < thresholdUsed). The orchestrator wraps this in a
39
+ * ToolError with the ParseConfidence payload attached as `details`. */
40
+ function isBelowThreshold(c) {
41
+ return c.overall < c.thresholdUsed;
42
+ }
43
+ /** Convenience: build the user-facing remediation question for an
44
+ * under-confidence recording. */
45
+ function buildLowConfidenceQuestion(c) {
46
+ const reasons = [];
47
+ for (const u of c.unknownContracts) {
48
+ reasons.push(`unknown contract ${u.contract} (${u.reason})`);
49
+ }
50
+ for (const o of c.opaqueScVals) {
51
+ reasons.push(`opaque ScVal at ${o.path} (${o.type})`);
52
+ }
53
+ const why = reasons.length === 0 ? 'no diagnostic reason available' : reasons.join('; ');
54
+ return `Recording refused: parseConfidence ${c.overall.toFixed(3)} is below the threshold ${c.thresholdUsed.toFixed(3)}. Diagnostic: ${why}. Supply an ABI for the unknown contract(s) or re-capture the transaction against a known protocol version, then re-run record_transaction.`;
55
+ }
@@ -0,0 +1,21 @@
1
+ import type { ToolResponse } from '../errors.ts';
2
+ import type { Network, RecordedTransaction } from '../types.ts';
3
+ import { type RpcFetcher } from './rpc.ts';
4
+ /** Public input shape. The brief pins:
5
+ * - exactly one of `hash` / `xdr`
6
+ * - `network` for hash mode (selects the public RPC)
7
+ * - optional injected `fetcher` for tests + custom RPC endpoints
8
+ * - optional `confidenceOverride` to relax the gate (default 1.0)
9
+ *
10
+ * In xdr mode `network` is still required so the caller documents intent
11
+ * (the synthesis downstream consumes it).
12
+ */
13
+ export interface RecordInput {
14
+ hash?: string;
15
+ xdr?: string;
16
+ network: Network;
17
+ fetcher?: RpcFetcher;
18
+ confidenceOverride?: number;
19
+ }
20
+ export type RecordResult = ToolResponse<RecordedTransaction>;
21
+ export declare function recordTransaction(input: RecordInput): Promise<RecordResult>;
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ // src/record/index.ts - the recorder orchestrator.
3
+ //
4
+ // `recordTransaction(input)` is the single public entry point for the
5
+ // synthesizer (the next phase) to consume. Two modes:
6
+ // - on-chain (hash set): fetch via the injected RPC fetcher (default:
7
+ // public Soroban RPC for the requested network).
8
+ // - simulation/XDR (xdr set): decode the provided envelope XDR directly.
9
+ //
10
+ // In both modes the recorder:
11
+ // 1. Decodes the envelope to sourceAccount, signers, invocations (+ diagnostic
12
+ // subInvocations), args -> ScVal subset.
13
+ // 2. Extracts token movements from raw events (or skips in XDR mode).
14
+ // 3. Validates the parsed movements/invocations against the raw events.
15
+ // 4. Computes parseConfidence.
16
+ // 5. Refuses with ToolError RECORDING_VALIDATION_FAILED when overall < threshold.
17
+ //
18
+ // Returns ToolResponse<RecordedTransaction> per the canonical envelope
19
+ // defined in src/errors.ts.
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.recordTransaction = recordTransaction;
22
+ const decode_ts_1 = require("./decode.js");
23
+ const freshness_ts_1 = require("./freshness.js");
24
+ const movements_ts_1 = require("./movements.js");
25
+ const rpc_ts_1 = require("./rpc.js");
26
+ const validate_ts_1 = require("./validate.js");
27
+ async function recordTransaction(input) {
28
+ if (!input.network) {
29
+ return err('RECORDING_FAILED', 'network required', false);
30
+ }
31
+ // A confidenceOverride outside [0, 1] would disable the fail-closed gate (a
32
+ // negative threshold can never be exceeded), so reject it up front.
33
+ if (input.confidenceOverride !== undefined &&
34
+ (!Number.isFinite(input.confidenceOverride) ||
35
+ input.confidenceOverride < 0 ||
36
+ input.confidenceOverride > 1)) {
37
+ return err('RECORDING_FAILED', 'confidenceOverride must be a finite number within [0, 1]', false);
38
+ }
39
+ const hasHash = typeof input.hash === 'string' && input.hash.length > 0;
40
+ const hasXdr = typeof input.xdr === 'string' && input.xdr.length > 0;
41
+ if (hasHash && hasXdr) {
42
+ return err('RECORDING_FAILED', 'provide exactly one of `hash` or `xdr`, not both', false);
43
+ }
44
+ if (!hasHash && !hasXdr) {
45
+ return err('RECORDING_FAILED', 'one of `hash` or `xdr` is required', false);
46
+ }
47
+ // === Phase 1: fetch + decode ===
48
+ let decoded;
49
+ if (hasHash) {
50
+ const fetcher = input.fetcher ?? (0, rpc_ts_1.createRpcServer)(input.network);
51
+ const hash = input.hash;
52
+ if (!hash)
53
+ return err('RECORDING_FAILED', 'hash required for on-chain mode', false);
54
+ const fetched = await fetcher(hash);
55
+ if (!fetched) {
56
+ return err('RECORDING_FAILED', `transaction ${hash} not found on ${input.network}`, true);
57
+ }
58
+ const events = combineEvents(fetched.events);
59
+ try {
60
+ decoded = (0, decode_ts_1.decodeEnvelope)(fetched.envelopeXdr, events, [], fetched.ledger, undefined, input.network);
61
+ }
62
+ catch (e) {
63
+ if (e instanceof decode_ts_1.DecodeError)
64
+ return err('RECORDING_FAILED', e.message, false);
65
+ throw e;
66
+ }
67
+ return finish(input.network, decoded, input.confidenceOverride);
68
+ }
69
+ // XDR mode has no raw on-chain events, so the events-based cross-check is
70
+ // skipped. parseConfidence reaches 1.0 when every invocation matches a
71
+ // known protocol interface or pinned address; a confidenceOverride is only
72
+ // needed when the caller wants to lower the gate below the default 1.0.
73
+ const xdrStr = input.xdr;
74
+ if (!xdrStr)
75
+ return err('RECORDING_FAILED', 'xdr required for simulation mode', false);
76
+ try {
77
+ decoded = (0, decode_ts_1.decodeEnvelopeXdr)(xdrStr, [], [], 0, undefined, input.network);
78
+ }
79
+ catch (e) {
80
+ if (e instanceof decode_ts_1.DecodeError)
81
+ return err('RECORDING_FAILED', e.message, false);
82
+ return err('RECORDING_FAILED', `failed to decode base64 envelope XDR: ${e.message}`, false);
83
+ }
84
+ return finish(input.network, decoded, input.confidenceOverride);
85
+ }
86
+ function combineEvents(raw) {
87
+ const a = (0, decode_ts_1.transactionEventsToOnChainEvents)(raw.transactionEventsXdr);
88
+ const b = (0, decode_ts_1.contractEventsToOnChainEvents)(raw.contractEventsXdr);
89
+ const seen = new Set();
90
+ const merged = [];
91
+ for (const e of [...a, ...b]) {
92
+ const k = `${e.contract}|${e.topics.join(',')}|${JSON.stringify(e.data)}`;
93
+ if (seen.has(k))
94
+ continue;
95
+ seen.add(k);
96
+ merged.push(e);
97
+ }
98
+ return merged;
99
+ }
100
+ /** Apply freshness + validate + refuse gate, then build the RecordedTransaction
101
+ * on success. */
102
+ function finish(network, decoded, confidenceOverride) {
103
+ // === Phase 2: extract token movements ===
104
+ const tokenMovements = (0, movements_ts_1.extractTokenMovements)(decoded.events, decoded.opaqueScVals);
105
+ // === Phase 3: validate against events (fail-closed) ===
106
+ if (decoded.events.length > 0) {
107
+ const failure = (0, validate_ts_1.validateAgainstEvents)(tokenMovements, decoded.events);
108
+ if (failure) {
109
+ const te = {
110
+ code: 'RECORDING_VALIDATION_FAILED',
111
+ message: failure.message,
112
+ severity: 'error',
113
+ retryable: false,
114
+ details: failure.details,
115
+ remediation: {
116
+ userQuestion: {
117
+ code: failure.code,
118
+ question: `Recording refused: ${failure.message}. Inspect details and re-run record_transaction against a re-fetched transaction.`,
119
+ },
120
+ },
121
+ };
122
+ return { ok: false, error: te };
123
+ }
124
+ }
125
+ // === Phase 4: parseConfidence + refuse gate ===
126
+ let confidence = (0, freshness_ts_1.computeParseConfidence)({
127
+ knownContracts: decoded.knownContracts,
128
+ unknownContracts: decoded.unknownContracts,
129
+ opaqueScVals: decoded.opaqueScVals,
130
+ });
131
+ if (typeof confidenceOverride === 'number') {
132
+ confidence = { ...confidence, thresholdUsed: confidenceOverride };
133
+ }
134
+ if ((0, freshness_ts_1.isBelowThreshold)(confidence)) {
135
+ const te = {
136
+ code: 'RECORDING_VALIDATION_FAILED',
137
+ message: `parseConfidence ${confidence.overall} < threshold ${confidence.thresholdUsed}`,
138
+ severity: 'error',
139
+ retryable: false,
140
+ details: confidence,
141
+ remediation: {
142
+ userQuestion: {
143
+ code: 'PARSE_CONFIDENCE_BELOW_THRESHOLD',
144
+ question: (0, freshness_ts_1.buildLowConfidenceQuestion)(confidence),
145
+ },
146
+ },
147
+ };
148
+ return { ok: false, error: te };
149
+ }
150
+ const recorded = {
151
+ network,
152
+ signers: decoded.signers,
153
+ invocations: decoded.invocations,
154
+ tokenMovements,
155
+ events: decoded.events,
156
+ authEntries: decoded.authEntries,
157
+ ledgerSequence: decoded.ledgerSequence,
158
+ fetchedAt: Math.floor(Date.now() / 1000),
159
+ parseConfidence: confidence,
160
+ sourceAccount: decoded.sourceAccount,
161
+ };
162
+ return { ok: true, data: recorded };
163
+ }
164
+ function err(code, message, retryable) {
165
+ return { ok: false, error: { code, message, severity: 'error', retryable } };
166
+ }
@@ -0,0 +1,20 @@
1
+ import type { xdr } from '@stellar/stellar-sdk';
2
+ import type { OnChainEvent, TokenMovement } from '../types.ts';
3
+ /** Extract TokenMovement[] from a list of raw on-chain events. Pure: no IO,
4
+ * no network. Non-movement events are ignored; malformed movement events are
5
+ * recorded as opaque so the recorder can fail closed. */
6
+ export declare function extractTokenMovements(events: OnChainEvent[], opaqueScVals?: Array<{
7
+ path: string;
8
+ type: string;
9
+ }>): TokenMovement[];
10
+ /** Read an I128 amount from an event data ScVal. Supports:
11
+ * - data is I128 directly
12
+ * - data is a Vec whose [0] (or [1] for SAC transfer) is I128
13
+ * - data is a Vec whose [0] is a Map { 'amount': I128 }
14
+ * - data is U64 (fallback for amount fields encoded as u64) */
15
+ export declare function readAmount(data: OnChainEvent['data']): string | null;
16
+ /** Re-decode a single ContractEvent body into a TokenMovement-shaped tuple,
17
+ * given the raw xdr.ScVal form (NOT the OnChainEvent subset). Used by the
18
+ * integration test to compare against the parse path without depending on
19
+ * the OnChainEvent subset. */
20
+ export declare function parseContractEventToMovement(emitterC: string, body: xdr.ContractEventV0): TokenMovement | null;