@crediolabs/policy-synth 0.1.4 → 0.1.6

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 (154) 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 +56 -3
  5. package/dist/record/freshness.d.ts +14 -1
  6. package/dist/record/freshness.js +32 -2
  7. package/dist/record/index.d.ts +11 -0
  8. package/dist/record/index.js +25 -0
  9. package/dist/record/movements.d.ts +15 -3
  10. package/dist/record/movements.js +42 -7
  11. package/dist/run/index.d.ts +43 -0
  12. package/dist/run/index.js +222 -0
  13. package/dist/run/schemas.d.ts +2016 -0
  14. package/dist/run/schemas.js +223 -0
  15. package/dist/synth/address.d.ts +7 -0
  16. package/dist/synth/address.js +12 -0
  17. package/dist/synth/compose-from-recording.d.ts +4 -3
  18. package/dist/synth/compose-from-recording.js +16 -6
  19. package/dist/synth/deny-cases.d.ts +12 -2
  20. package/dist/synth/deny-cases.js +76 -4
  21. package/dist/synth/evaluate.js +2 -2
  22. package/dist/synth/index.d.ts +1 -0
  23. package/dist/synth/index.js +4 -0
  24. package/dist/synth/minimize.d.ts +1 -1
  25. package/dist/synth/minimize.js +3 -3
  26. package/dist/synth/synthesize-from-recording.d.ts +5 -2
  27. package/dist/synth/synthesize-from-recording.js +175 -69
  28. package/dist/types.d.ts +41 -1
  29. package/dist/types.js +17 -0
  30. package/dist/verify/simulate.js +18 -2
  31. package/dist/verify/verify.js +18 -2
  32. package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
  33. package/dist-cjs/adapters/interpreter/adapter.js +527 -0
  34. package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
  35. package/dist-cjs/adapters/interpreter/index.js +8 -0
  36. package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
  37. package/dist-cjs/adapters/oz/adapter.js +289 -0
  38. package/dist-cjs/adapters/oz/index.d.ts +1 -0
  39. package/dist-cjs/adapters/oz/index.js +8 -0
  40. package/dist-cjs/codegen/compile-gate.d.ts +33 -0
  41. package/dist-cjs/codegen/compile-gate.js +123 -0
  42. package/dist-cjs/codegen/index.d.ts +2 -0
  43. package/dist-cjs/codegen/index.js +14 -0
  44. package/dist-cjs/codegen/template.d.ts +18 -0
  45. package/dist-cjs/codegen/template.js +134 -0
  46. package/dist-cjs/errors.d.ts +37 -0
  47. package/dist-cjs/errors.js +3 -0
  48. package/dist-cjs/index.d.ts +11 -0
  49. package/dist-cjs/index.js +27 -0
  50. package/dist-cjs/ir/index.d.ts +1 -0
  51. package/dist-cjs/ir/index.js +3 -0
  52. package/dist-cjs/ir/types.d.ts +108 -0
  53. package/dist-cjs/ir/types.js +12 -0
  54. package/dist-cjs/mandate/index.d.ts +2 -0
  55. package/dist-cjs/mandate/index.js +6 -0
  56. package/dist-cjs/mandate/to-ir.d.ts +3 -0
  57. package/dist-cjs/mandate/to-ir.js +63 -0
  58. package/dist-cjs/mandate/types.d.ts +20 -0
  59. package/dist-cjs/mandate/types.js +9 -0
  60. package/dist-cjs/package.json +3 -0
  61. package/dist-cjs/predicate/encode.d.ts +10 -0
  62. package/dist-cjs/predicate/encode.js +252 -0
  63. package/dist-cjs/predicate/index.d.ts +1 -0
  64. package/dist-cjs/predicate/index.js +6 -0
  65. package/dist-cjs/record/decode.d.ts +79 -0
  66. package/dist-cjs/record/decode.js +441 -0
  67. package/dist-cjs/record/freshness.d.ts +30 -0
  68. package/dist-cjs/record/freshness.js +85 -0
  69. package/dist-cjs/record/index.d.ts +32 -0
  70. package/dist-cjs/record/index.js +191 -0
  71. package/dist-cjs/record/movements.d.ts +32 -0
  72. package/dist-cjs/record/movements.js +227 -0
  73. package/dist-cjs/record/rpc.d.ts +22 -0
  74. package/dist-cjs/record/rpc.js +74 -0
  75. package/dist-cjs/record/validate.d.ts +22 -0
  76. package/dist-cjs/record/validate.js +63 -0
  77. package/dist-cjs/registry/identify.d.ts +11 -0
  78. package/dist-cjs/registry/identify.js +87 -0
  79. package/dist-cjs/registry/index.d.ts +3 -0
  80. package/dist-cjs/registry/index.js +15 -0
  81. package/dist-cjs/registry/known-addresses.d.ts +16 -0
  82. package/dist-cjs/registry/known-addresses.js +53 -0
  83. package/dist-cjs/registry/protocols.d.ts +38 -0
  84. package/dist-cjs/registry/protocols.js +153 -0
  85. package/dist-cjs/review-card/builder.d.ts +14 -0
  86. package/dist-cjs/review-card/builder.js +264 -0
  87. package/dist-cjs/review-card/conflict.d.ts +40 -0
  88. package/dist-cjs/review-card/conflict.js +114 -0
  89. package/dist-cjs/review-card/cross-check.d.ts +11 -0
  90. package/dist-cjs/review-card/cross-check.js +151 -0
  91. package/dist-cjs/review-card/index.d.ts +3 -0
  92. package/dist-cjs/review-card/index.js +10 -0
  93. package/dist-cjs/run/index.d.ts +43 -0
  94. package/dist-cjs/run/index.js +237 -0
  95. package/dist-cjs/run/schemas.d.ts +2016 -0
  96. package/dist-cjs/run/schemas.js +226 -0
  97. package/dist-cjs/seams/index.d.ts +1 -0
  98. package/dist-cjs/seams/index.js +3 -0
  99. package/dist-cjs/seams/types.d.ts +66 -0
  100. package/dist-cjs/seams/types.js +12 -0
  101. package/dist-cjs/synth/address.d.ts +7 -0
  102. package/dist-cjs/synth/address.js +15 -0
  103. package/dist-cjs/synth/compose-from-recording.d.ts +65 -0
  104. package/dist-cjs/synth/compose-from-recording.js +366 -0
  105. package/dist-cjs/synth/deny-cases.d.ts +22 -0
  106. package/dist-cjs/synth/deny-cases.js +438 -0
  107. package/dist-cjs/synth/evaluate.d.ts +39 -0
  108. package/dist-cjs/synth/evaluate.js +428 -0
  109. package/dist-cjs/synth/harness.d.ts +16 -0
  110. package/dist-cjs/synth/harness.js +29 -0
  111. package/dist-cjs/synth/index.d.ts +10 -0
  112. package/dist-cjs/synth/index.js +28 -0
  113. package/dist-cjs/synth/lower.d.ts +23 -0
  114. package/dist-cjs/synth/lower.js +119 -0
  115. package/dist-cjs/synth/minimize.d.ts +4 -0
  116. package/dist-cjs/synth/minimize.js +41 -0
  117. package/dist-cjs/synth/predicate-literals.d.ts +5 -0
  118. package/dist-cjs/synth/predicate-literals.js +28 -0
  119. package/dist-cjs/synth/scope.d.ts +26 -0
  120. package/dist-cjs/synth/scope.js +81 -0
  121. package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
  122. package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
  123. package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
  124. package/dist-cjs/synth/synthesize-from-recording.js +759 -0
  125. package/dist-cjs/types.d.ts +311 -0
  126. package/dist-cjs/types.js +55 -0
  127. package/dist-cjs/verify/envelope.d.ts +15 -0
  128. package/dist-cjs/verify/envelope.js +23 -0
  129. package/dist-cjs/verify/index.d.ts +3 -0
  130. package/dist-cjs/verify/index.js +8 -0
  131. package/dist-cjs/verify/simulate.d.ts +31 -0
  132. package/dist-cjs/verify/simulate.js +261 -0
  133. package/dist-cjs/verify/verify.d.ts +21 -0
  134. package/dist-cjs/verify/verify.js +192 -0
  135. package/package.json +43 -3
  136. package/src/codegen/template.ts +3 -3
  137. package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +171 -0
  138. package/src/record/corpus-fixtures.json +532 -0
  139. package/src/record/decode.ts +63 -10
  140. package/src/record/freshness.ts +34 -2
  141. package/src/record/index.ts +40 -0
  142. package/src/record/movements.ts +40 -7
  143. package/src/run/index.ts +277 -0
  144. package/src/run/schemas.ts +250 -0
  145. package/src/synth/address.ts +14 -0
  146. package/src/synth/compose-from-recording.ts +20 -9
  147. package/src/synth/deny-cases.ts +87 -4
  148. package/src/synth/evaluate.ts +2 -2
  149. package/src/synth/index.ts +4 -0
  150. package/src/synth/minimize.ts +7 -3
  151. package/src/synth/synthesize-from-recording.ts +200 -68
  152. package/src/types.ts +37 -1
  153. package/src/verify/simulate.ts +21 -2
  154. package/src/verify/verify.ts +21 -2
@@ -0,0 +1,441 @@
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 'scvMap': {
287
+ if (depth >= types_ts_1.MAX_SCVAL_DEPTH) {
288
+ opaqueScVals.push({ path, type: 'depth-exceeded' });
289
+ return { type: 'other', value: 'depth-exceeded' };
290
+ }
291
+ // Map<Symbol, ScVal> is the canonical data shape for SAC/SEP-41
292
+ // transfer/mint/burn events on Stellar mainnet (verified empirically
293
+ // against txs 112d2392..., eb39f493..., 50d36f5c...). The entry key is
294
+ // carried forward so readAmount can route to the conventional field
295
+ // name without re-decoding the full XDR. Non-symbol / non-string keys
296
+ // are stringified via the same key-to-string rule used for topics.
297
+ const entries = (val.map() ?? []);
298
+ return {
299
+ type: 'map',
300
+ value: entries.map((entry, i) => ({
301
+ key: mapKeyToString(entry.key(), `${path}.key[${i}]`, opaqueScVals),
302
+ val: scValToSubset(entry.val(), `${path}.val[${i}]`, opaqueScVals, depth + 1),
303
+ })),
304
+ };
305
+ }
306
+ case 'scvBytes': {
307
+ return { type: 'bytes', value: Buffer.from(val.bytes()).toString('hex') };
308
+ }
309
+ default: {
310
+ opaqueScVals.push({ path, type: kind });
311
+ return { type: 'other', value: kind };
312
+ }
313
+ }
314
+ }
315
+ /** Convert Int128Parts (hi: Int64, lo: Uint64) to a BigInt.
316
+ * hi is a signed 64-bit value; lo is unsigned. We use the SDK-provided
317
+ * `toString()` form which gives the signed decimal representation of the 64-bit
318
+ * Int64 directly. */
319
+ function i128PartsToBigInt(parts) {
320
+ const hiStr = parts.hi().toString();
321
+ const loStr = parts.lo().toString();
322
+ const hi = BigInt(hiStr);
323
+ const lo = BigInt(loStr);
324
+ return (hi << 64n) + lo;
325
+ }
326
+ /** Convert UInt128Parts (hi: Uint64, lo: Uint64) to an unsigned BigInt. */
327
+ function u128PartsToBigInt(parts) {
328
+ const hi = BigInt(parts.hi().toString());
329
+ const lo = BigInt(parts.lo().toString());
330
+ return (hi << 64n) + lo;
331
+ }
332
+ function u64PartsToBigInt(h) {
333
+ return BigInt(h.toString());
334
+ }
335
+ function recordInvocation(contract, fn, args, network, knownSet, knownContractSet, knownOut, unknownOut) {
336
+ // Recognition is evaluated PER INVOCATION using the (contract, fn, args)
337
+ // triple. The first call to a contract is no more authoritative than the
338
+ // second: an unrecognized call always contributes to the unknown count
339
+ // regardless of whether another invocation to the same contract was
340
+ // recognised. knownOut is deduplicated by contract (so the
341
+ // parseConfidence denominator isn't inflated by repeat calls), but each
342
+ // unrecognised invocation is recorded separately so the numerator
343
+ // reflects every fail-closed hit.
344
+ if (knownSet.has(contract)) {
345
+ if (!knownContractSet.has(contract)) {
346
+ knownContractSet.add(contract);
347
+ knownOut.push(contract);
348
+ }
349
+ return;
350
+ }
351
+ if ((0, identify_ts_1.identifyProtocol)(contract, fn, args, network ?? undefined)) {
352
+ if (!knownContractSet.has(contract)) {
353
+ knownContractSet.add(contract);
354
+ knownOut.push(contract);
355
+ }
356
+ return;
357
+ }
358
+ unknownOut.push({ contract, reason: 'no-abi' });
359
+ }
360
+ /** Convert a ScVal to a friendly string for use in OnChainEvent.topics
361
+ * (always-string scalar representation; vectors/bytes are stringified as JSON
362
+ * to keep the field string-typed). */
363
+ function scValToTopicString(val) {
364
+ const kind = val.switch().name;
365
+ if (kind === 'scvSymbol')
366
+ return val.sym().toString();
367
+ if (kind === 'scvString')
368
+ return val.str().toString();
369
+ if (kind === 'scvAddress') {
370
+ const strkey = decodeScAddressToAnyStrkey(val.address());
371
+ return strkey ?? `<unparsed-address:${kind}>`;
372
+ }
373
+ if (kind === 'scvU64')
374
+ return u64PartsToBigInt(val.u64()).toString();
375
+ if (kind === 'scvU32')
376
+ return val.u32().toString();
377
+ if (kind === 'scvI128')
378
+ return i128PartsToBigInt(val.i128()).toString();
379
+ if (kind === 'scvBytes')
380
+ return Buffer.from(val.bytes()).toString('hex');
381
+ return `<${kind}>`;
382
+ }
383
+ /** Convert a Map<Symbol, ...> key ScVal to the string used in the normalised
384
+ * ScVal map representation. Uses the same string-form rule as
385
+ * `scValToTopicString` so SAC/SEP-41 event Map keys ("amount", "to_muxed_id")
386
+ * resolve to the same string in both the topic path and the map-entry path.
387
+ * Non-symbol / non-string keys record an opaque diagnostic and return a
388
+ * placeholder so the Map entry is still carried forward. */
389
+ function mapKeyToString(val, path, opaqueScVals) {
390
+ const kind = val.switch().name;
391
+ if (kind === 'scvSymbol')
392
+ return val.sym().toString();
393
+ if (kind === 'scvString')
394
+ return val.str().toString();
395
+ if (kind === 'scvU64')
396
+ return u64PartsToBigInt(val.u64()).toString();
397
+ if (kind === 'scvU32')
398
+ return val.u32().toString();
399
+ if (kind === 'scvI128')
400
+ return i128PartsToBigInt(val.i128()).toString();
401
+ if (kind === 'scvBytes')
402
+ return Buffer.from(val.bytes()).toString('hex');
403
+ opaqueScVals.push({ path, type: `unsupported-map-key:${kind}` });
404
+ return `<${kind}>`;
405
+ }
406
+ /** Build the OnChainEvent[] view from raw contract events. Diagnostic-only:
407
+ * captures the topics + data + emitter contract for the downstream
408
+ * validator. */
409
+ function contractEventsToOnChainEvents(contractEvents) {
410
+ const out = [];
411
+ for (const group of contractEvents) {
412
+ for (const evt of group) {
413
+ const body = evt.body().v0();
414
+ const emitter = evt.contractId()
415
+ ? stellar_sdk_1.Address.contract(Buffer.from(evt.contractId())).toString()
416
+ : '';
417
+ const topics = body.topics().map((t) => scValToTopicString(t));
418
+ const data = scValToSubset(body.data(), 'event.data', []);
419
+ out.push({ contract: emitter, topics, data });
420
+ }
421
+ }
422
+ return out;
423
+ }
424
+ /** Build the OnChainEvent[] view from TransactionEvent[] (which wraps
425
+ * ContractEvent with a stage). Same shape as above. */
426
+ function transactionEventsToOnChainEvents(txEvents) {
427
+ const contractEvents = txEvents.map((e) => [e.event()]);
428
+ return contractEventsToOnChainEvents(contractEvents);
429
+ }
430
+ /** Tiny typed error so callers can `instanceof DecodeError` without leaking
431
+ * SDK internals. */
432
+ class DecodeError extends Error {
433
+ name = 'DecodeError';
434
+ }
435
+ exports.DecodeError = DecodeError;
436
+ /** Helper used by validate.ts to map a TokenMovement back to a search key
437
+ * without re-parsing (the validate module must cross-check against the raw
438
+ * events, NOT against the parse). */
439
+ function tokenMovementKey(m) {
440
+ return `${m.token}|${m.from}|${m.to}|${m.amount}`;
441
+ }
@@ -0,0 +1,30 @@
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
+ *
18
+ * The remediation text branches on which diagnostic bucket is non-empty:
19
+ * - unknownContracts only -> user MUST supply an ABI (or re-capture
20
+ * against a known protocol version). The contract is real, the
21
+ * recorder just cannot decode it.
22
+ * - opaqueScVals only -> the recorder encountered a value shape it
23
+ * should support but did not decode. The user cannot supply an ABI
24
+ * to fix a decoder bug; the guidance explicitly says so and points at
25
+ * re-running after a tool upgrade / reporting the path.
26
+ * - both -> list both diagnostics AND chain the right remediation for
27
+ * each (the user supplies an ABI AND reports the decoder gap).
28
+ * - neither (the "denom === 0" path) -> unchanged from the pre-fix
29
+ * text; the user did not actually hit a code-level barrier. */
30
+ export declare function buildLowConfidenceQuestion(c: ParseConfidence): string;
@@ -0,0 +1,85 @@
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
+ *
46
+ * The remediation text branches on which diagnostic bucket is non-empty:
47
+ * - unknownContracts only -> user MUST supply an ABI (or re-capture
48
+ * against a known protocol version). The contract is real, the
49
+ * recorder just cannot decode it.
50
+ * - opaqueScVals only -> the recorder encountered a value shape it
51
+ * should support but did not decode. The user cannot supply an ABI
52
+ * to fix a decoder bug; the guidance explicitly says so and points at
53
+ * re-running after a tool upgrade / reporting the path.
54
+ * - both -> list both diagnostics AND chain the right remediation for
55
+ * each (the user supplies an ABI AND reports the decoder gap).
56
+ * - neither (the "denom === 0" path) -> unchanged from the pre-fix
57
+ * text; the user did not actually hit a code-level barrier. */
58
+ function buildLowConfidenceQuestion(c) {
59
+ const hasUnknown = c.unknownContracts.length > 0;
60
+ const hasOpaque = c.opaqueScVals.length > 0;
61
+ const reasons = [];
62
+ for (const u of c.unknownContracts) {
63
+ reasons.push(`unknown contract ${u.contract} (${u.reason})`);
64
+ }
65
+ for (const o of c.opaqueScVals) {
66
+ reasons.push(`opaque ScVal at ${o.path} (${o.type})`);
67
+ }
68
+ const why = reasons.length === 0 ? 'no diagnostic reason available' : reasons.join('; ');
69
+ const header = `Recording refused: parseConfidence ${c.overall.toFixed(3)} is below the threshold ${c.thresholdUsed.toFixed(3)}. Diagnostic: ${why}.`;
70
+ // The two remediation branches are explicit so the user (or the agent)
71
+ // can dispatch on the verb. The "unknown contract" branch is the only
72
+ // one that asks for an ABI; the "opaque ScVal" branch asks the user to
73
+ // report the path and re-run after a tool upgrade.
74
+ if (hasUnknown && hasOpaque) {
75
+ return (`${header} Supply an ABI for the unknown contract(s) or re-capture the transaction against a known protocol version; ` +
76
+ `also file the opaque ScVal path against the recorder (decoder limitation) and re-run record_transaction after a tool upgrade.`);
77
+ }
78
+ if (hasUnknown) {
79
+ return `${header} Supply an ABI for the unknown contract(s) or re-capture the transaction against a known protocol version, then re-run record_transaction.`;
80
+ }
81
+ if (hasOpaque) {
82
+ return `${header} This is a recorder decoder limitation (the value shape is supported in principle but not yet decoded) - the agent cannot fix it by supplying an ABI. Report the opaque ScVal path above against the recorder and re-run record_transaction after a tool upgrade.`;
83
+ }
84
+ return `${header} No actionable diagnostic was recorded; re-run record_transaction against a fresh fetch and inspect the events.`;
85
+ }
@@ -0,0 +1,32 @@
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 `crossNetworkFetcher` (item 2) for tests + offline use;
9
+ * production builds the cross-network probe from `createRpcServer` when
10
+ * this field is absent
11
+ * - optional `confidenceOverride` to relax the gate (default 1.0)
12
+ *
13
+ * In xdr mode `network` is still required so the caller documents intent
14
+ * (the synthesis downstream consumes it).
15
+ */
16
+ export interface RecordInput {
17
+ hash?: string;
18
+ xdr?: string;
19
+ network: Network;
20
+ fetcher?: RpcFetcher;
21
+ /** Test-only seam + explicit production override for the cross-network
22
+ * probe (item 2). When the primary fetcher returns null on hash mode,
23
+ * the recorder also tries this fetcher against the OTHER network. If it
24
+ * finds the hash there, the recorder returns a specific actionable
25
+ * error mentioning the actual network. When unset, the recorder builds
26
+ * this fetcher from `createRpcServer(otherNetwork)` so the help fires
27
+ * automatically; tests can pass a deterministic stub. */
28
+ crossNetworkFetcher?: RpcFetcher;
29
+ confidenceOverride?: number;
30
+ }
31
+ export type RecordResult = ToolResponse<RecordedTransaction>;
32
+ export declare function recordTransaction(input: RecordInput): Promise<RecordResult>;