@crediolabs/policy-synth 0.1.0

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 (85) hide show
  1. package/dist/adapters/oz/adapter.d.ts +20 -0
  2. package/dist/adapters/oz/adapter.js +282 -0
  3. package/dist/adapters/oz/index.d.ts +1 -0
  4. package/dist/adapters/oz/index.js +2 -0
  5. package/dist/errors.d.ts +37 -0
  6. package/dist/errors.js +2 -0
  7. package/dist/index.d.ts +9 -0
  8. package/dist/index.js +9 -0
  9. package/dist/ir/index.d.ts +1 -0
  10. package/dist/ir/index.js +2 -0
  11. package/dist/ir/types.d.ts +97 -0
  12. package/dist/ir/types.js +11 -0
  13. package/dist/mandate/index.d.ts +2 -0
  14. package/dist/mandate/index.js +2 -0
  15. package/dist/mandate/to-ir.d.ts +3 -0
  16. package/dist/mandate/to-ir.js +60 -0
  17. package/dist/mandate/types.d.ts +20 -0
  18. package/dist/mandate/types.js +8 -0
  19. package/dist/record/decode.d.ts +76 -0
  20. package/dist/record/decode.js +372 -0
  21. package/dist/record/freshness.d.ts +17 -0
  22. package/dist/record/freshness.js +50 -0
  23. package/dist/record/index.d.ts +21 -0
  24. package/dist/record/index.js +163 -0
  25. package/dist/record/movements.d.ts +20 -0
  26. package/dist/record/movements.js +187 -0
  27. package/dist/record/rpc.d.ts +22 -0
  28. package/dist/record/rpc.js +70 -0
  29. package/dist/record/validate.d.ts +22 -0
  30. package/dist/record/validate.js +60 -0
  31. package/dist/registry/identify.d.ts +11 -0
  32. package/dist/registry/identify.js +84 -0
  33. package/dist/registry/index.d.ts +3 -0
  34. package/dist/registry/index.js +4 -0
  35. package/dist/registry/known-addresses.d.ts +16 -0
  36. package/dist/registry/known-addresses.js +49 -0
  37. package/dist/registry/protocols.d.ts +38 -0
  38. package/dist/registry/protocols.js +149 -0
  39. package/dist/seams/index.d.ts +1 -0
  40. package/dist/seams/index.js +2 -0
  41. package/dist/seams/types.d.ts +66 -0
  42. package/dist/seams/types.js +11 -0
  43. package/dist/synth/compose-from-recording.d.ts +36 -0
  44. package/dist/synth/compose-from-recording.js +162 -0
  45. package/dist/synth/index.d.ts +5 -0
  46. package/dist/synth/index.js +6 -0
  47. package/dist/synth/lower.d.ts +23 -0
  48. package/dist/synth/lower.js +116 -0
  49. package/dist/synth/scope.d.ts +26 -0
  50. package/dist/synth/scope.js +77 -0
  51. package/dist/synth/synthesize-from-mandate.d.ts +5 -0
  52. package/dist/synth/synthesize-from-mandate.js +34 -0
  53. package/dist/synth/synthesize-from-recording.d.ts +17 -0
  54. package/dist/synth/synthesize-from-recording.js +178 -0
  55. package/dist/types.d.ts +249 -0
  56. package/dist/types.js +35 -0
  57. package/package.json +37 -0
  58. package/src/adapters/oz/adapter.ts +363 -0
  59. package/src/adapters/oz/index.ts +9 -0
  60. package/src/errors.ts +79 -0
  61. package/src/index.ts +9 -0
  62. package/src/ir/index.ts +13 -0
  63. package/src/ir/types.ts +94 -0
  64. package/src/mandate/index.ts +4 -0
  65. package/src/mandate/to-ir.ts +71 -0
  66. package/src/mandate/types.ts +21 -0
  67. package/src/record/decode.ts +500 -0
  68. package/src/record/freshness.ts +63 -0
  69. package/src/record/index.ts +224 -0
  70. package/src/record/movements.ts +188 -0
  71. package/src/record/rpc.ts +88 -0
  72. package/src/record/validate.ts +75 -0
  73. package/src/registry/identify.ts +99 -0
  74. package/src/registry/index.ts +24 -0
  75. package/src/registry/known-addresses.ts +71 -0
  76. package/src/registry/protocols.ts +176 -0
  77. package/src/seams/index.ts +11 -0
  78. package/src/seams/types.ts +81 -0
  79. package/src/synth/compose-from-recording.ts +226 -0
  80. package/src/synth/index.ts +19 -0
  81. package/src/synth/lower.ts +144 -0
  82. package/src/synth/scope.ts +108 -0
  83. package/src/synth/synthesize-from-mandate.ts +46 -0
  84. package/src/synth/synthesize-from-recording.ts +226 -0
  85. package/src/types.ts +218 -0
@@ -0,0 +1,76 @@
1
+ import { xdr } from '@stellar/stellar-sdk';
2
+ import type { ContractInvocation, Network, OnChainEvent, ParseConfidence, ScVal, TokenMovement } from '../types.ts';
3
+ /** Result of decoding an envelope XDR plus the surrounding metadata from
4
+ * `getTransaction` (events + auth entries). All fields are plain JSON; the
5
+ * caller decides how to interpret them. */
6
+ export interface SerializableAuthEntry {
7
+ authorizingAddress: string | null;
8
+ contract: string;
9
+ fn: string;
10
+ }
11
+ export interface DecodedTransaction {
12
+ sourceAccount: string;
13
+ /** Authorising signers from the envelope signatures (hex-encoded ed25519
14
+ * pubkey hints expanded to G... strkeys). The recorder cannot recover the
15
+ * strkey from the 4-byte hint alone; we record the hint bytes hex instead
16
+ * when no full strkey is available. */
17
+ signers: string[];
18
+ invocations: ContractInvocation[];
19
+ /** Raw on-chain events (from getTransaction.events). Used by validate.ts to
20
+ * cross-check the parsed TokenMovement[] and ContractInvocation[]. */
21
+ events: OnChainEvent[];
22
+ /** Plain JSON projection of InvokeHostFunctionOp.auth(). */
23
+ authEntries: SerializableAuthEntry[];
24
+ /** Records that include at least one `ContractInvocation` whose decoded args
25
+ * vector or whose auth-tree contained an opaque ScVal. Empty list = full
26
+ * parseConfidence = 1.0 (modulo unknown contracts). */
27
+ opaqueScVals: ParseConfidence['opaqueScVals'];
28
+ /** Contracts referenced in invocations whose (fn, args) did not match any
29
+ * known protocol ABI. Each unrecognised invocation is recorded separately
30
+ * so the freshness gate stays fail-closed regardless of operation order. */
31
+ unknownContracts: ParseConfidence['unknownContracts'];
32
+ knownContracts: string[];
33
+ ledgerSequence: number;
34
+ }
35
+ /** Decode a TransactionEnvelope XDR (base64) into a fully parsed invocation tree. */
36
+ export declare function decodeEnvelopeXdr(envelopeXdrB64: string, events?: OnChainEvent[], _authEntries?: unknown[], ledgerSequence?: number, knownContracts?: ReadonlySet<string>, network?: Network | null): DecodedTransaction;
37
+ /** Same as `decodeEnvelopeXdr` but accepts an already-decoded envelope. Useful
38
+ * for tests that build the envelope via SDK helpers (no XDR round-trip). */
39
+ export declare function decodeEnvelope(envelope: xdr.TransactionEnvelope, events?: OnChainEvent[], _authEntries?: unknown[], ledgerSequence?: number, knownContracts?: ReadonlySet<string>, network?: Network | null): DecodedTransaction;
40
+ /** Decode an ScAddress (either account or contract) to its strkey form. */
41
+ export declare function decodeScAddressToAnyStrkey(scAddr: xdr.ScAddress): string | null;
42
+ /** Decode an ScAddress to a C... contract strkey. */
43
+ export declare function decodeScAddressToC(scAddr: xdr.ScAddress): string;
44
+ /** Map an arbitrary ScVal into the normalised `ScVal` subset defined in
45
+ * types.ts. Anything outside the subset is wrapped as `{type:'other', value}`
46
+ * AND recorded in `opaqueScVals` so the freshness module can reduce
47
+ * parseConfidence. */
48
+ export declare function scValToSubset(val: xdr.ScVal, path: string, opaqueScVals: ParseConfidence['opaqueScVals']): ScVal;
49
+ /** Convert Int128Parts (hi: Int64, lo: Uint64) to a BigInt.
50
+ * hi is a signed 64-bit value; lo is unsigned. We use the SDK-provided
51
+ * `toString()` form which gives the signed decimal representation of the 64-bit
52
+ * Int64 directly. */
53
+ export declare function i128PartsToBigInt(parts: xdr.Int128Parts): bigint;
54
+ /** Convert UInt128Parts (hi: Uint64, lo: Uint64) to an unsigned BigInt. */
55
+ export declare function u128PartsToBigInt(parts: xdr.UInt128Parts): bigint;
56
+ export declare function u64PartsToBigInt(h: xdr.UnsignedHyper | xdr.Hyper): bigint;
57
+ /** Convert a ScVal to a friendly string for use in OnChainEvent.topics
58
+ * (always-string scalar representation; vectors/bytes are stringified as JSON
59
+ * to keep the field string-typed). */
60
+ export declare function scValToTopicString(val: xdr.ScVal): string;
61
+ /** Build the OnChainEvent[] view from raw contract events. Diagnostic-only:
62
+ * captures the topics + data + emitter contract for the downstream
63
+ * validator. */
64
+ export declare function contractEventsToOnChainEvents(contractEvents: xdr.ContractEvent[][]): OnChainEvent[];
65
+ /** Build the OnChainEvent[] view from TransactionEvent[] (which wraps
66
+ * ContractEvent with a stage). Same shape as above. */
67
+ export declare function transactionEventsToOnChainEvents(txEvents: xdr.TransactionEvent[]): OnChainEvent[];
68
+ /** Tiny typed error so callers can `instanceof DecodeError` without leaking
69
+ * SDK internals. */
70
+ export declare class DecodeError extends Error {
71
+ readonly name = "DecodeError";
72
+ }
73
+ /** Helper used by validate.ts to map a TokenMovement back to a search key
74
+ * without re-parsing (the validate module must cross-check against the raw
75
+ * events, NOT against the parse). */
76
+ export declare function tokenMovementKey(m: TokenMovement): string;
@@ -0,0 +1,372 @@
1
+ // src/record/decode.ts - pure XDR -> invocation tree decoder.
2
+ //
3
+ // Inputs: a base64-encoded TransactionEnvelope XDR (or one already decoded),
4
+ // the raw transaction events (used only to identify contract emitters), and
5
+ // (optionally) the auth-entry set.
6
+ //
7
+ // Outputs:
8
+ // - top-level ContractInvocation (the single Context `Policy::enforce` receives)
9
+ // - subInvocations captured on each ContractInvocation from
10
+ // SorobanAuthorizedInvocation.subInvocations - DIAGNOSTIC ONLY;
11
+ // v1 grammar does NOT walk sub-invocations (see types.ts notes).
12
+ // - decoded args vector mapped to the normalised `ScVal` subset
13
+ // - sourceAccount, signers
14
+ // - raw event list surfaced for downstream validation
15
+ // - auth entries surfaced for downstream validation
16
+ //
17
+ // No network calls. No randomness. No globals.
18
+ import { Address, StrKey, xdr } from '@stellar/stellar-sdk';
19
+ import { identifyProtocol } from "../registry/identify.js";
20
+ /** Decode a TransactionEnvelope XDR (base64) into a fully parsed invocation tree. */
21
+ export function decodeEnvelopeXdr(envelopeXdrB64, events = [], _authEntries = [], ledgerSequence = 0, knownContracts = new Set(), network = null) {
22
+ const envelope = xdr.TransactionEnvelope.fromXDR(envelopeXdrB64, 'base64');
23
+ return decodeEnvelope(envelope, events, _authEntries, ledgerSequence, knownContracts, network);
24
+ }
25
+ /** Same as `decodeEnvelopeXdr` but accepts an already-decoded envelope. Useful
26
+ * for tests that build the envelope via SDK helpers (no XDR round-trip). */
27
+ export function decodeEnvelope(envelope, events = [], _authEntries = [], ledgerSequence = 0, knownContracts = new Set(), network = null) {
28
+ const envType = envelope.switch().name;
29
+ if (envType === 'envelopeTypeTxFeeBump') {
30
+ // Fee-bump wraps a normal inner v1 transaction (a different account
31
+ // pays the fee). The real operations + their authorizers live on the
32
+ // INNER v1 envelope, so decode that and discard the outer fee-bump
33
+ // shell. The fee-bump envelope switch arm `v0` is intentionally not
34
+ // routed here - fee-bump v0 does not exist.
35
+ const innerV1 = envelope.feeBump().tx().innerTx().v1();
36
+ return decodeV1Envelope(innerV1, events, ledgerSequence, knownContracts, network);
37
+ }
38
+ if (envType !== 'envelopeTypeTx') {
39
+ // TransactionV0 is out of scope - v1 protocol only. Fee-bump is now
40
+ // handled above; legacy envelopes reach this branch.
41
+ throw new DecodeError(`unsupported envelope: ${envType}`);
42
+ }
43
+ return decodeV1Envelope(envelope.v1(), events, ledgerSequence, knownContracts, network);
44
+ }
45
+ /** Decode a v1 TransactionEnvelope (either a top-level v1 envelope or the
46
+ * inner v1 envelope of a fee-bump wrapper). Source, operations, auth
47
+ * entries, signers all come from this v1 envelope. */
48
+ function decodeV1Envelope(v1, events, ledgerSequence, knownContracts, network) {
49
+ const tx = v1.tx();
50
+ const sourceAccount = decodeMuxedOrEd25519(tx.sourceAccount());
51
+ const signers = extractSigners(v1, tx);
52
+ const invocations = [];
53
+ const projectedAuthEntries = [];
54
+ const opaqueScVals = [];
55
+ const knownContractSet = new Set();
56
+ const unknownContracts = [];
57
+ const knownContractsOut = [];
58
+ for (const op of tx.operations()) {
59
+ const body = op.body();
60
+ if (body.switch().name !== 'invokeHostFunction')
61
+ continue;
62
+ const invokeOp = body.invokeHostFunctionOp();
63
+ const auth = invokeOp.auth();
64
+ projectedAuthEntries.push(...auth.map(projectAuthEntry));
65
+ const hostFn = invokeOp.hostFunction();
66
+ if (hostFn.switch().name !== 'hostFunctionTypeInvokeContract')
67
+ continue;
68
+ const invokeArgs = hostFn.invokeContract();
69
+ const contract = decodeScAddressToC(invokeArgs.contractAddress());
70
+ const fn = symbolValueFromBufferOrString(invokeArgs.functionName());
71
+ const argsScval = invokeArgs.args();
72
+ const args = [];
73
+ argsScval.forEach((a, i) => {
74
+ args.push(scValToSubset(a, `args[${i}]`, opaqueScVals));
75
+ });
76
+ const subInvocations = decodeSubInvocations(auth, opaqueScVals);
77
+ invocations.push({ contract, fn, args, subInvocations });
78
+ recordInvocation(contract, fn, args, network, knownContracts, knownContractSet, knownContractsOut, unknownContracts);
79
+ }
80
+ return {
81
+ sourceAccount,
82
+ signers,
83
+ invocations,
84
+ events,
85
+ authEntries: projectedAuthEntries,
86
+ opaqueScVals,
87
+ unknownContracts,
88
+ knownContracts: knownContractsOut,
89
+ ledgerSequence,
90
+ };
91
+ }
92
+ /** Extract the union of signers from:
93
+ * - envelope-level signatures (the tx-level signers), and
94
+ * - Soroban auth entries that are `SorobanCredentials.sorobanCredentialsAddress`
95
+ * (the contract-Caller authorised via ed25519/address credentials).
96
+ * Returns G... strkeys (or address strkeys for non-account credentials). */
97
+ function extractSigners(v1, tx) {
98
+ const set = new Set();
99
+ for (const sig of v1.signatures()) {
100
+ // Each signature has a 4-byte hint. We cannot recover the full strkey from
101
+ // a 4-byte hint alone - so we record it as `hint:<hex>` to preserve signal
102
+ // for the downstream reviewer without claiming a match we cannot prove.
103
+ const hintBytes = sig.hint();
104
+ set.add(`hint:${Buffer.from(hintBytes).toString('hex')}`);
105
+ }
106
+ // Soroban address credentials - recover the strkey address.
107
+ for (const op of tx.operations()) {
108
+ const body = op.body();
109
+ if (body.switch().name !== 'invokeHostFunction')
110
+ continue;
111
+ const invokeOp = body.invokeHostFunctionOp();
112
+ for (const entry of invokeOp.auth()) {
113
+ const creds = entry.credentials();
114
+ const kind = creds.switch().name;
115
+ if (kind === 'sorobanCredentialsAddress') {
116
+ const addrCreds = creds.address();
117
+ const scAddr = addrCreds.address();
118
+ try {
119
+ const strkey = decodeScAddressToAnyStrkey(scAddr);
120
+ if (strkey)
121
+ set.add(strkey);
122
+ }
123
+ catch {
124
+ // ignore - we record it as opaque via freshness
125
+ }
126
+ }
127
+ // sorobanCredentialsSourceAccount -> pulled from tx.sourceAccount() above
128
+ }
129
+ }
130
+ return Array.from(set);
131
+ }
132
+ const MAX_AUTH_TREE_DEPTH = 16;
133
+ function projectAuthEntry(entry) {
134
+ const credentials = entry.credentials();
135
+ const authorizingAddress = credentials.switch().name === 'sorobanCredentialsAddress'
136
+ ? decodeScAddressToAnyStrkey(credentials.address().address())
137
+ : null;
138
+ const fn = entry.rootInvocation().function();
139
+ if (fn.switch().name === 'sorobanAuthorizedFunctionTypeContractFn') {
140
+ const contractFn = fn.contractFn();
141
+ return {
142
+ authorizingAddress,
143
+ contract: decodeScAddressToC(contractFn.contractAddress()),
144
+ fn: symbolValueFromBufferOrString(contractFn.functionName()),
145
+ };
146
+ }
147
+ return { authorizingAddress, contract: '', fn: '__create_contract__' };
148
+ }
149
+ function decodeSubInvocations(authEntries, opaqueScVals) {
150
+ return authEntries.flatMap((entry, index) => {
151
+ const decoded = decodeAuthorizedInvocation(entry.rootInvocation(), opaqueScVals, `auth[${index}].root`, 0);
152
+ return decoded ? [decoded] : [];
153
+ });
154
+ }
155
+ function decodeAuthorizedInvocation(inv, opaqueScVals, path, depth) {
156
+ if (depth >= MAX_AUTH_TREE_DEPTH) {
157
+ opaqueScVals.push({ path, type: 'auth-tree-depth-exceeded' });
158
+ return null;
159
+ }
160
+ const fn = inv.function();
161
+ if (fn.switch().name !== 'sorobanAuthorizedFunctionTypeContractFn')
162
+ return null;
163
+ const contractFn = fn.contractFn();
164
+ const contract = decodeScAddressToC(contractFn.contractAddress());
165
+ const fnName = symbolValueFromBufferOrString(contractFn.functionName());
166
+ const args = contractFn
167
+ .args()
168
+ .map((arg, index) => scValToSubset(arg, `${path}.args[${index}]`, opaqueScVals));
169
+ const subInvocations = inv.subInvocations().flatMap((child, index) => {
170
+ const decoded = decodeAuthorizedInvocation(child, opaqueScVals, `${path}.subInvocations[${index}]`, depth + 1);
171
+ return decoded ? [decoded] : [];
172
+ });
173
+ return { contract, fn: fnName, args, subInvocations };
174
+ }
175
+ /** Decode an ScAddress (either account or contract) to its strkey form. */
176
+ export function decodeScAddressToAnyStrkey(scAddr) {
177
+ const kind = scAddr.switch().name;
178
+ if (kind === 'scAddressTypeAccount') {
179
+ return decodePublicKeyToStrkey(scAddr.accountId());
180
+ }
181
+ if (kind === 'scAddressTypeContract') {
182
+ const hashBytes = scAddr.contractId();
183
+ return Address.contract(Buffer.from(hashBytes)).toString();
184
+ }
185
+ return null;
186
+ }
187
+ /** Decode an ScAddress to a C... contract strkey. */
188
+ export function decodeScAddressToC(scAddr) {
189
+ if (scAddr.switch().name === 'scAddressTypeContract') {
190
+ const hashBytes = scAddr.contractId();
191
+ return Address.contract(Buffer.from(hashBytes)).toString();
192
+ }
193
+ if (scAddr.switch().name === 'scAddressTypeAccount') {
194
+ const strkey = decodePublicKeyToStrkey(scAddr.accountId());
195
+ if (strkey)
196
+ return strkey;
197
+ }
198
+ throw new DecodeError(`cannot convert ScAddress ${scAddr.switch().name} to contract strkey`);
199
+ }
200
+ /** Decode a tx source account (MuxedEd25519Account OR AccountId OR PublicKey).
201
+ * - muxedAccount -> underlying ed25519 account as G...
202
+ * - publicKeyTypeEd25519 -> G...
203
+ */
204
+ function decodeMuxedOrEd25519(src) {
205
+ const kind = src.switch().name;
206
+ if (kind === 'keyTypeMuxedEd25519') {
207
+ const ed = src.med25519().ed25519();
208
+ return StrKey.encodeEd25519PublicKey(ed);
209
+ }
210
+ const ed = src.ed25519();
211
+ return StrKey.encodeEd25519PublicKey(ed);
212
+ }
213
+ /** AccountId is a type alias for PublicKey in this SDK; extract the ed25519
214
+ * strkey from a PublicKey union. */
215
+ function decodePublicKeyToStrkey(pubKey) {
216
+ if (pubKey.switch().name === 'publicKeyTypeEd25519') {
217
+ return StrKey.encodeEd25519PublicKey(pubKey.ed25519());
218
+ }
219
+ return null;
220
+ }
221
+ /** `functionName()` returns `string | Buffer` per the SDK type definition; we
222
+ * accept both and normalise. */
223
+ function symbolValueFromBufferOrString(s) {
224
+ return Buffer.isBuffer(s) ? s.toString('utf8') : s;
225
+ }
226
+ /** Map an arbitrary ScVal into the normalised `ScVal` subset defined in
227
+ * types.ts. Anything outside the subset is wrapped as `{type:'other', value}`
228
+ * AND recorded in `opaqueScVals` so the freshness module can reduce
229
+ * parseConfidence. */
230
+ export function scValToSubset(val, path, opaqueScVals) {
231
+ const kind = val.switch().name;
232
+ switch (kind) {
233
+ case 'scvAddress': {
234
+ const strkey = decodeScAddressToAnyStrkey(val.address());
235
+ if (strkey === null) {
236
+ opaqueScVals.push({ path, type: kind });
237
+ return { type: 'other', value: kind };
238
+ }
239
+ return { type: 'address', value: strkey };
240
+ }
241
+ case 'scvI128': {
242
+ const parts = val.i128();
243
+ return { type: 'i128', value: i128PartsToBigInt(parts).toString() };
244
+ }
245
+ case 'scvU64': {
246
+ return { type: 'u64', value: u64PartsToBigInt(val.u64()).toString() };
247
+ }
248
+ case 'scvU32': {
249
+ return { type: 'u32', value: val.u32().toString() };
250
+ }
251
+ case 'scvSymbol': {
252
+ return { type: 'symbol', value: val.sym().toString() };
253
+ }
254
+ case 'scvVec': {
255
+ const arr = (val.vec() ?? []);
256
+ return {
257
+ type: 'vec',
258
+ value: arr.map((v, i) => scValToSubset(v, `${path}[${i}]`, opaqueScVals)),
259
+ };
260
+ }
261
+ case 'scvBytes': {
262
+ return { type: 'bytes', value: Buffer.from(val.bytes()).toString('hex') };
263
+ }
264
+ default: {
265
+ opaqueScVals.push({ path, type: kind });
266
+ return { type: 'other', value: kind };
267
+ }
268
+ }
269
+ }
270
+ /** Convert Int128Parts (hi: Int64, lo: Uint64) to a BigInt.
271
+ * hi is a signed 64-bit value; lo is unsigned. We use the SDK-provided
272
+ * `toString()` form which gives the signed decimal representation of the 64-bit
273
+ * Int64 directly. */
274
+ export function i128PartsToBigInt(parts) {
275
+ const hiStr = parts.hi().toString();
276
+ const loStr = parts.lo().toString();
277
+ const hi = BigInt(hiStr);
278
+ const lo = BigInt(loStr);
279
+ return (hi << 64n) + lo;
280
+ }
281
+ /** Convert UInt128Parts (hi: Uint64, lo: Uint64) to an unsigned BigInt. */
282
+ export function u128PartsToBigInt(parts) {
283
+ const hi = BigInt(parts.hi().toString());
284
+ const lo = BigInt(parts.lo().toString());
285
+ return (hi << 64n) + lo;
286
+ }
287
+ export function u64PartsToBigInt(h) {
288
+ return BigInt(h.toString());
289
+ }
290
+ function recordInvocation(contract, fn, args, network, knownSet, knownContractSet, knownOut, unknownOut) {
291
+ // Recognition is evaluated PER INVOCATION using the (contract, fn, args)
292
+ // triple. The first call to a contract is no more authoritative than the
293
+ // second: an unrecognized call always contributes to the unknown count
294
+ // regardless of whether another invocation to the same contract was
295
+ // recognised. knownOut is deduplicated by contract (so the
296
+ // parseConfidence denominator isn't inflated by repeat calls), but each
297
+ // unrecognised invocation is recorded separately so the numerator
298
+ // reflects every fail-closed hit.
299
+ if (knownSet.has(contract)) {
300
+ if (!knownContractSet.has(contract)) {
301
+ knownContractSet.add(contract);
302
+ knownOut.push(contract);
303
+ }
304
+ return;
305
+ }
306
+ if (identifyProtocol(contract, fn, args, network ?? undefined)) {
307
+ if (!knownContractSet.has(contract)) {
308
+ knownContractSet.add(contract);
309
+ knownOut.push(contract);
310
+ }
311
+ return;
312
+ }
313
+ unknownOut.push({ contract, reason: 'no-abi' });
314
+ }
315
+ /** Convert a ScVal to a friendly string for use in OnChainEvent.topics
316
+ * (always-string scalar representation; vectors/bytes are stringified as JSON
317
+ * to keep the field string-typed). */
318
+ export function scValToTopicString(val) {
319
+ const kind = val.switch().name;
320
+ if (kind === 'scvSymbol')
321
+ return val.sym().toString();
322
+ if (kind === 'scvString')
323
+ return val.str().toString();
324
+ if (kind === 'scvAddress') {
325
+ const strkey = decodeScAddressToAnyStrkey(val.address());
326
+ return strkey ?? `<unparsed-address:${kind}>`;
327
+ }
328
+ if (kind === 'scvU64')
329
+ return u64PartsToBigInt(val.u64()).toString();
330
+ if (kind === 'scvU32')
331
+ return val.u32().toString();
332
+ if (kind === 'scvI128')
333
+ return i128PartsToBigInt(val.i128()).toString();
334
+ if (kind === 'scvBytes')
335
+ return Buffer.from(val.bytes()).toString('hex');
336
+ return `<${kind}>`;
337
+ }
338
+ /** Build the OnChainEvent[] view from raw contract events. Diagnostic-only:
339
+ * captures the topics + data + emitter contract for the downstream
340
+ * validator. */
341
+ export function contractEventsToOnChainEvents(contractEvents) {
342
+ const out = [];
343
+ for (const group of contractEvents) {
344
+ for (const evt of group) {
345
+ const body = evt.body().v0();
346
+ const emitter = evt.contractId()
347
+ ? Address.contract(Buffer.from(evt.contractId())).toString()
348
+ : '';
349
+ const topics = body.topics().map((t) => scValToTopicString(t));
350
+ const data = scValToSubset(body.data(), 'event.data', []);
351
+ out.push({ contract: emitter, topics, data });
352
+ }
353
+ }
354
+ return out;
355
+ }
356
+ /** Build the OnChainEvent[] view from TransactionEvent[] (which wraps
357
+ * ContractEvent with a stage). Same shape as above. */
358
+ export function transactionEventsToOnChainEvents(txEvents) {
359
+ const contractEvents = txEvents.map((e) => [e.event()]);
360
+ return contractEventsToOnChainEvents(contractEvents);
361
+ }
362
+ /** Tiny typed error so callers can `instanceof DecodeError` without leaking
363
+ * SDK internals. */
364
+ export class DecodeError extends Error {
365
+ name = 'DecodeError';
366
+ }
367
+ /** Helper used by validate.ts to map a TokenMovement back to a search key
368
+ * without re-parsing (the validate module must cross-check against the raw
369
+ * events, NOT against the parse). */
370
+ export function tokenMovementKey(m) {
371
+ return `${m.token}|${m.from}|${m.to}|${m.amount}`;
372
+ }
@@ -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,50 @@
1
+ // src/record/freshness.ts - compute parseConfidence for a DecodedTransaction.
2
+ //
3
+ // The pinned rule (see ParseConfidence in src/types.ts):
4
+ // overall = 1 - (unknownContracts + opaqueScVals) / total
5
+ // where total = unique contracts referenced + opaque ScVal count.
6
+ // In v1 every contract is "unknown" by default (no adjacency ABI hint is
7
+ // bundled with the recorder); the freshness module still computes the value
8
+ // honestly so callers can see why the gate fires.
9
+ //
10
+ // thresholdUsed defaults to 1.0 - a stricter gate. Callers can opt to lower
11
+ // it via confidence_override but v1 ships only the default.
12
+ /** Compute parseConfidence per the pinned rule. Pure: no IO. */
13
+ export function computeParseConfidence(input) {
14
+ const unknown = input.unknownContracts.length;
15
+ const opaque = input.opaqueScVals.length;
16
+ const known = input.knownContracts.length;
17
+ // Pinned rule: overall = 1 - (unknown + opaque) / (known + unknown + opaque).
18
+ // Only the empty-decode case (nothing referenced) is guarded to avoid 0/0; it
19
+ // means "nothing to decode", so full confidence. A fully-unknown tx therefore
20
+ // scores 0, not an inflated value.
21
+ const denom = known + unknown + opaque;
22
+ const total = denom === 0 ? 1 : denom;
23
+ const overall = Math.max(0, Math.min(1, 1 - (unknown + opaque) / total));
24
+ return {
25
+ overall,
26
+ knownContracts: [...input.knownContracts],
27
+ unknownContracts: [...input.unknownContracts],
28
+ opaqueScVals: [...input.opaqueScVals],
29
+ thresholdUsed: 1.0,
30
+ };
31
+ }
32
+ /** Refuse-on-low-confidence gate. Returns true when the recording must be
33
+ * refused (overall < thresholdUsed). The orchestrator wraps this in a
34
+ * ToolError with the ParseConfidence payload attached as `details`. */
35
+ export function isBelowThreshold(c) {
36
+ return c.overall < c.thresholdUsed;
37
+ }
38
+ /** Convenience: build the user-facing remediation question for an
39
+ * under-confidence recording. */
40
+ export function buildLowConfidenceQuestion(c) {
41
+ const reasons = [];
42
+ for (const u of c.unknownContracts) {
43
+ reasons.push(`unknown contract ${u.contract} (${u.reason})`);
44
+ }
45
+ for (const o of c.opaqueScVals) {
46
+ reasons.push(`opaque ScVal at ${o.path} (${o.type})`);
47
+ }
48
+ const why = reasons.length === 0 ? 'no diagnostic reason available' : reasons.join('; ');
49
+ 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.`;
50
+ }
@@ -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>;