@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.
- package/LICENSE +21 -0
- package/dist/codegen/template.js +3 -3
- package/dist/record/decode.d.ts +6 -3
- package/dist/record/decode.js +56 -3
- package/dist/record/freshness.d.ts +14 -1
- package/dist/record/freshness.js +32 -2
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +25 -0
- package/dist/record/movements.d.ts +15 -3
- package/dist/record/movements.js +42 -7
- package/dist/run/index.d.ts +43 -0
- package/dist/run/index.js +222 -0
- package/dist/run/schemas.d.ts +2016 -0
- package/dist/run/schemas.js +223 -0
- package/dist/synth/address.d.ts +7 -0
- package/dist/synth/address.js +12 -0
- package/dist/synth/compose-from-recording.d.ts +4 -3
- package/dist/synth/compose-from-recording.js +16 -6
- package/dist/synth/deny-cases.d.ts +12 -2
- package/dist/synth/deny-cases.js +76 -4
- package/dist/synth/evaluate.js +2 -2
- package/dist/synth/index.d.ts +1 -0
- package/dist/synth/index.js +4 -0
- package/dist/synth/minimize.d.ts +1 -1
- package/dist/synth/minimize.js +3 -3
- package/dist/synth/synthesize-from-recording.d.ts +5 -2
- package/dist/synth/synthesize-from-recording.js +175 -69
- package/dist/types.d.ts +41 -1
- package/dist/types.js +17 -0
- package/dist/verify/simulate.js +18 -2
- package/dist/verify/verify.js +18 -2
- package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
- package/dist-cjs/adapters/interpreter/adapter.js +527 -0
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
- package/dist-cjs/adapters/interpreter/index.js +8 -0
- package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
- package/dist-cjs/adapters/oz/adapter.js +289 -0
- package/dist-cjs/adapters/oz/index.d.ts +1 -0
- package/dist-cjs/adapters/oz/index.js +8 -0
- package/dist-cjs/codegen/compile-gate.d.ts +33 -0
- package/dist-cjs/codegen/compile-gate.js +123 -0
- package/dist-cjs/codegen/index.d.ts +2 -0
- package/dist-cjs/codegen/index.js +14 -0
- package/dist-cjs/codegen/template.d.ts +18 -0
- package/dist-cjs/codegen/template.js +134 -0
- package/dist-cjs/errors.d.ts +37 -0
- package/dist-cjs/errors.js +3 -0
- package/dist-cjs/index.d.ts +11 -0
- package/dist-cjs/index.js +27 -0
- package/dist-cjs/ir/index.d.ts +1 -0
- package/dist-cjs/ir/index.js +3 -0
- package/dist-cjs/ir/types.d.ts +108 -0
- package/dist-cjs/ir/types.js +12 -0
- package/dist-cjs/mandate/index.d.ts +2 -0
- package/dist-cjs/mandate/index.js +6 -0
- package/dist-cjs/mandate/to-ir.d.ts +3 -0
- package/dist-cjs/mandate/to-ir.js +63 -0
- package/dist-cjs/mandate/types.d.ts +20 -0
- package/dist-cjs/mandate/types.js +9 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/predicate/encode.d.ts +10 -0
- package/dist-cjs/predicate/encode.js +252 -0
- package/dist-cjs/predicate/index.d.ts +1 -0
- package/dist-cjs/predicate/index.js +6 -0
- package/dist-cjs/record/decode.d.ts +79 -0
- package/dist-cjs/record/decode.js +441 -0
- package/dist-cjs/record/freshness.d.ts +30 -0
- package/dist-cjs/record/freshness.js +85 -0
- package/dist-cjs/record/index.d.ts +32 -0
- package/dist-cjs/record/index.js +191 -0
- package/dist-cjs/record/movements.d.ts +32 -0
- package/dist-cjs/record/movements.js +227 -0
- package/dist-cjs/record/rpc.d.ts +22 -0
- package/dist-cjs/record/rpc.js +74 -0
- package/dist-cjs/record/validate.d.ts +22 -0
- package/dist-cjs/record/validate.js +63 -0
- package/dist-cjs/registry/identify.d.ts +11 -0
- package/dist-cjs/registry/identify.js +87 -0
- package/dist-cjs/registry/index.d.ts +3 -0
- package/dist-cjs/registry/index.js +15 -0
- package/dist-cjs/registry/known-addresses.d.ts +16 -0
- package/dist-cjs/registry/known-addresses.js +53 -0
- package/dist-cjs/registry/protocols.d.ts +38 -0
- package/dist-cjs/registry/protocols.js +153 -0
- package/dist-cjs/review-card/builder.d.ts +14 -0
- package/dist-cjs/review-card/builder.js +264 -0
- package/dist-cjs/review-card/conflict.d.ts +40 -0
- package/dist-cjs/review-card/conflict.js +114 -0
- package/dist-cjs/review-card/cross-check.d.ts +11 -0
- package/dist-cjs/review-card/cross-check.js +151 -0
- package/dist-cjs/review-card/index.d.ts +3 -0
- package/dist-cjs/review-card/index.js +10 -0
- package/dist-cjs/run/index.d.ts +43 -0
- package/dist-cjs/run/index.js +237 -0
- package/dist-cjs/run/schemas.d.ts +2016 -0
- package/dist-cjs/run/schemas.js +226 -0
- package/dist-cjs/seams/index.d.ts +1 -0
- package/dist-cjs/seams/index.js +3 -0
- package/dist-cjs/seams/types.d.ts +66 -0
- package/dist-cjs/seams/types.js +12 -0
- package/dist-cjs/synth/address.d.ts +7 -0
- package/dist-cjs/synth/address.js +15 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +65 -0
- package/dist-cjs/synth/compose-from-recording.js +366 -0
- package/dist-cjs/synth/deny-cases.d.ts +22 -0
- package/dist-cjs/synth/deny-cases.js +438 -0
- package/dist-cjs/synth/evaluate.d.ts +39 -0
- package/dist-cjs/synth/evaluate.js +428 -0
- package/dist-cjs/synth/harness.d.ts +16 -0
- package/dist-cjs/synth/harness.js +29 -0
- package/dist-cjs/synth/index.d.ts +10 -0
- package/dist-cjs/synth/index.js +28 -0
- package/dist-cjs/synth/lower.d.ts +23 -0
- package/dist-cjs/synth/lower.js +119 -0
- package/dist-cjs/synth/minimize.d.ts +4 -0
- package/dist-cjs/synth/minimize.js +41 -0
- package/dist-cjs/synth/predicate-literals.d.ts +5 -0
- package/dist-cjs/synth/predicate-literals.js +28 -0
- package/dist-cjs/synth/scope.d.ts +26 -0
- package/dist-cjs/synth/scope.js +81 -0
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
- package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
- package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
- package/dist-cjs/synth/synthesize-from-recording.js +759 -0
- package/dist-cjs/types.d.ts +311 -0
- package/dist-cjs/types.js +55 -0
- package/dist-cjs/verify/envelope.d.ts +15 -0
- package/dist-cjs/verify/envelope.js +23 -0
- package/dist-cjs/verify/index.d.ts +3 -0
- package/dist-cjs/verify/index.js +8 -0
- package/dist-cjs/verify/simulate.d.ts +31 -0
- package/dist-cjs/verify/simulate.js +261 -0
- package/dist-cjs/verify/verify.d.ts +21 -0
- package/dist-cjs/verify/verify.js +192 -0
- package/package.json +43 -3
- package/src/codegen/template.ts +3 -3
- package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +171 -0
- package/src/record/corpus-fixtures.json +532 -0
- package/src/record/decode.ts +63 -10
- package/src/record/freshness.ts +34 -2
- package/src/record/index.ts +40 -0
- package/src/record/movements.ts +40 -7
- package/src/run/index.ts +277 -0
- package/src/run/schemas.ts +250 -0
- package/src/synth/address.ts +14 -0
- package/src/synth/compose-from-recording.ts +20 -9
- package/src/synth/deny-cases.ts +87 -4
- package/src/synth/evaluate.ts +2 -2
- package/src/synth/index.ts +4 -0
- package/src/synth/minimize.ts +7 -3
- package/src/synth/synthesize-from-recording.ts +200 -68
- package/src/types.ts +37 -1
- package/src/verify/simulate.ts +21 -2
- 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>;
|