@grimoirelabs/core 0.15.0 → 0.17.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.
- package/dist/compiler/grimoire/transformer.d.ts.map +1 -1
- package/dist/compiler/grimoire/transformer.js +25 -2
- package/dist/compiler/grimoire/transformer.js.map +1 -1
- package/dist/compiler/ir-generator.js +20 -1
- package/dist/compiler/ir-generator.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/interpreter.d.ts +36 -1
- package/dist/runtime/interpreter.d.ts.map +1 -1
- package/dist/runtime/interpreter.js +366 -90
- package/dist/runtime/interpreter.js.map +1 -1
- package/dist/types/actions.d.ts +18 -1
- package/dist/types/actions.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/receipt.d.ts +12 -0
- package/dist/types/receipt.d.ts.map +1 -1
- package/dist/venues/types.d.ts +13 -0
- package/dist/venues/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ import type { SpellIR } from "../types/ir.js";
|
|
|
7
7
|
import type { PolicySet } from "../types/policy.js";
|
|
8
8
|
import type { Address, ChainId } from "../types/primitives.js";
|
|
9
9
|
import type { QueryProvider } from "../types/query-provider.js";
|
|
10
|
-
import type { CommitResult, DriftKey, DriftPolicy, PreviewResult, Receipt } from "../types/receipt.js";
|
|
10
|
+
import type { BuildTransactionsResult, CommitResult, DriftKey, DriftPolicy, PreviewResult, Receipt } from "../types/receipt.js";
|
|
11
11
|
import type { VenueAdapter } from "../venues/types.js";
|
|
12
12
|
import { type ExecutionMode } from "../wallet/executor.js";
|
|
13
13
|
import { type Provider } from "../wallet/provider.js";
|
|
@@ -122,9 +122,44 @@ export interface CommitOptions {
|
|
|
122
122
|
* Commit a receipt — executes planned actions from the preview.
|
|
123
123
|
*/
|
|
124
124
|
export declare function commit(options: CommitOptions): Promise<CommitResult>;
|
|
125
|
+
/**
|
|
126
|
+
* Options for building unsigned transactions from a preview receipt
|
|
127
|
+
*/
|
|
128
|
+
export interface BuildTransactionsOptions {
|
|
129
|
+
receipt: Receipt;
|
|
130
|
+
walletAddress: Address;
|
|
131
|
+
provider?: Provider;
|
|
132
|
+
rpcUrl?: string;
|
|
133
|
+
adapters?: VenueAdapter[];
|
|
134
|
+
driftPolicy?: DriftPolicy;
|
|
135
|
+
driftValues?: Record<string, unknown>;
|
|
136
|
+
resolveDriftValue?: (key: DriftKey) => Promise<unknown>;
|
|
137
|
+
progressCallback?: (message: string) => void;
|
|
138
|
+
/**
|
|
139
|
+
* Secret used to verify receipt integrity for cross-process receipts.
|
|
140
|
+
* Required when the receipt was not issued by this process (i.e. not in
|
|
141
|
+
* the in-memory issuedReceipts map). Use signReceipt() at preview time
|
|
142
|
+
* to generate the matching integrity hash.
|
|
143
|
+
*/
|
|
144
|
+
receiptSecret?: string;
|
|
145
|
+
/** HMAC integrity hash produced by signReceipt(receipt, secret) */
|
|
146
|
+
receiptIntegrity?: string;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Build unsigned transactions from a preview receipt.
|
|
150
|
+
* Returns calldata for client-side signing (e.g. Privy SDK).
|
|
151
|
+
* Does NOT commit the receipt — commit() can still be called afterwards.
|
|
152
|
+
*/
|
|
153
|
+
export declare function buildTransactions(options: BuildTransactionsOptions): Promise<BuildTransactionsResult>;
|
|
125
154
|
/**
|
|
126
155
|
* Execute a compiled spell (backward-compatible wrapper).
|
|
127
156
|
* Internally uses preview() and, if needed, commit().
|
|
128
157
|
*/
|
|
129
158
|
export declare function execute(options: ExecuteOptions): Promise<ExecutionResult>;
|
|
159
|
+
/**
|
|
160
|
+
* Sign a receipt for cross-process integrity verification.
|
|
161
|
+
* Call this at preview time, persist the returned hex string alongside
|
|
162
|
+
* the receipt, and pass it as `receiptIntegrity` to buildTransactions().
|
|
163
|
+
*/
|
|
164
|
+
export declare function signReceipt(receipt: Receipt, secret: string): string;
|
|
130
165
|
//# sourceMappingURL=interpreter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interpreter.d.ts","sourceRoot":"","sources":["../../src/runtime/interpreter.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"interpreter.d.ts","sourceRoot":"","sources":["../../src/runtime/interpreter.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Spell Interpreter
|
|
3
3
|
* Executes compiled SpellIR via the preview/commit model.
|
|
4
4
|
*/
|
|
5
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
5
6
|
import { createVenueRegistry } from "../venues/index.js";
|
|
6
7
|
import { createExecutor } from "../wallet/executor.js";
|
|
7
8
|
import { createProvider } from "../wallet/provider.js";
|
|
9
|
+
import { TransactionBuilder } from "../wallet/tx-builder.js";
|
|
8
10
|
import { CircuitBreakerManager } from "./circuit-breaker.js";
|
|
9
11
|
import { createContext, getPersistentStateObject, InMemoryLedger, incrementAdvisoryCalls, markStepExecuted, } from "./context.js";
|
|
10
12
|
import { createEvalContext, evaluateAsync } from "./expression-evaluator.js";
|
|
@@ -318,98 +320,32 @@ export async function commit(options) {
|
|
|
318
320
|
};
|
|
319
321
|
}
|
|
320
322
|
}
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
ledgerEvents: ledger.getEntries(),
|
|
341
|
-
error: structuredError,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
let resolvedValue;
|
|
346
|
-
try {
|
|
347
|
-
resolvedValue = await resolveCommitDriftValue(driftKey, options);
|
|
348
|
-
}
|
|
349
|
-
catch (error) {
|
|
350
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
351
|
-
const structuredError = createStructuredError("commit", "DRIFT_RESOLUTION_FAILED", `Failed to resolve drift value for '${driftKey.field}': ${message}`, {
|
|
352
|
-
constraint: "drift_keys",
|
|
353
|
-
path: driftKey.field,
|
|
354
|
-
});
|
|
355
|
-
ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
|
|
356
|
-
return {
|
|
357
|
-
success: false,
|
|
358
|
-
receiptId: receipt.id,
|
|
359
|
-
transactions: [],
|
|
360
|
-
driftChecks,
|
|
361
|
-
finalState: receipt.finalState,
|
|
362
|
-
ledgerEvents: ledger.getEntries(),
|
|
363
|
-
error: structuredError,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
if (!resolvedValue.found && options.driftPolicy) {
|
|
367
|
-
const structuredError = createStructuredError("commit", "DRIFT_VALUE_MISSING", `Missing commit-time drift value for '${driftKey.field}'`, {
|
|
368
|
-
constraint: "drift_keys",
|
|
369
|
-
path: driftKey.field,
|
|
370
|
-
suggestion: "Provide driftValues for this key or configure resolveDriftValue to fetch commit-time values.",
|
|
371
|
-
});
|
|
372
|
-
ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
|
|
373
|
-
return {
|
|
374
|
-
success: false,
|
|
375
|
-
receiptId: receipt.id,
|
|
376
|
-
transactions: [],
|
|
377
|
-
driftChecks,
|
|
378
|
-
finalState: receipt.finalState,
|
|
379
|
-
ledgerEvents: ledger.getEntries(),
|
|
380
|
-
error: structuredError,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
const commitValue = resolvedValue.found ? resolvedValue.value : driftKey.previewValue;
|
|
384
|
-
const driftResult = evaluateDriftKey(driftKey, commitValue, options.driftPolicy);
|
|
385
|
-
driftChecks.push(driftResult);
|
|
323
|
+
const driftResult = await performDriftChecks(receipt.driftKeys, {
|
|
324
|
+
driftPolicy: options.driftPolicy,
|
|
325
|
+
driftValues: options.driftValues,
|
|
326
|
+
resolveDriftValue: options.resolveDriftValue,
|
|
327
|
+
});
|
|
328
|
+
if (driftResult.error) {
|
|
329
|
+
ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
receiptId: receipt.id,
|
|
333
|
+
transactions: [],
|
|
334
|
+
driftChecks: driftResult.driftChecks,
|
|
335
|
+
finalState: receipt.finalState,
|
|
336
|
+
ledgerEvents: ledger.getEntries(),
|
|
337
|
+
error: driftResult.error,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const driftChecks = driftResult.driftChecks;
|
|
341
|
+
for (const check of driftChecks) {
|
|
386
342
|
ledger.emit({
|
|
387
343
|
type: "drift_check",
|
|
388
|
-
field:
|
|
389
|
-
passed:
|
|
390
|
-
previewValue:
|
|
391
|
-
commitValue:
|
|
344
|
+
field: check.field,
|
|
345
|
+
passed: check.passed,
|
|
346
|
+
previewValue: check.previewValue,
|
|
347
|
+
commitValue: check.commitValue,
|
|
392
348
|
});
|
|
393
|
-
if (!driftResult.passed) {
|
|
394
|
-
const tolerance = resolveToleranceBps(driftKey, options.driftPolicy);
|
|
395
|
-
const structuredError = createStructuredError("commit", "DRIFT_EXCEEDED", `Drift exceeded for '${driftKey.field}'`, {
|
|
396
|
-
constraint: "drift_policy",
|
|
397
|
-
actual: driftResult.driftBps,
|
|
398
|
-
limit: tolerance,
|
|
399
|
-
path: driftKey.field,
|
|
400
|
-
suggestion: "Run preview again or increase drift tolerance for this key class.",
|
|
401
|
-
});
|
|
402
|
-
ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
|
|
403
|
-
return {
|
|
404
|
-
success: false,
|
|
405
|
-
receiptId: receipt.id,
|
|
406
|
-
transactions: [],
|
|
407
|
-
driftChecks,
|
|
408
|
-
finalState: receipt.finalState,
|
|
409
|
-
ledgerEvents: ledger.getEntries(),
|
|
410
|
-
error: structuredError,
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
349
|
}
|
|
414
350
|
// Execute planned actions
|
|
415
351
|
const { chainId } = receipt.chainContext;
|
|
@@ -505,6 +441,124 @@ export async function commit(options) {
|
|
|
505
441
|
ledgerEvents: ledger.getEntries(),
|
|
506
442
|
};
|
|
507
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Build unsigned transactions from a preview receipt.
|
|
446
|
+
* Returns calldata for client-side signing (e.g. Privy SDK).
|
|
447
|
+
* Does NOT commit the receipt — commit() can still be called afterwards.
|
|
448
|
+
*/
|
|
449
|
+
export async function buildTransactions(options) {
|
|
450
|
+
const { receipt } = options;
|
|
451
|
+
// Validate receipt status
|
|
452
|
+
if (receipt.status !== "ready") {
|
|
453
|
+
return {
|
|
454
|
+
success: false,
|
|
455
|
+
receiptId: receipt.id,
|
|
456
|
+
transactions: [],
|
|
457
|
+
driftChecks: [],
|
|
458
|
+
error: createStructuredError("commit", "RECEIPT_INVALID_STATUS", `Receipt status is '${receipt.status}', expected 'ready'`),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
// Validate receipt shape, integrity, and committed status
|
|
462
|
+
const validationError = validateBuildReceipt(receipt, {
|
|
463
|
+
receiptSecret: options.receiptSecret,
|
|
464
|
+
receiptIntegrity: options.receiptIntegrity,
|
|
465
|
+
});
|
|
466
|
+
if (validationError) {
|
|
467
|
+
return {
|
|
468
|
+
success: false,
|
|
469
|
+
receiptId: receipt.id,
|
|
470
|
+
transactions: [],
|
|
471
|
+
driftChecks: [],
|
|
472
|
+
error: validationError,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
// Check receipt age
|
|
476
|
+
if (options.driftPolicy?.maxAge) {
|
|
477
|
+
const ageSec = (Date.now() - receipt.timestamp) / 1000;
|
|
478
|
+
if (ageSec > options.driftPolicy.maxAge) {
|
|
479
|
+
return {
|
|
480
|
+
success: false,
|
|
481
|
+
receiptId: receipt.id,
|
|
482
|
+
transactions: [],
|
|
483
|
+
driftChecks: [],
|
|
484
|
+
error: createStructuredError("commit", "RECEIPT_EXPIRED", `Receipt expired: age ${Math.round(ageSec)}s exceeds maxAge ${options.driftPolicy.maxAge}s`, {
|
|
485
|
+
actual: Math.round(ageSec),
|
|
486
|
+
limit: options.driftPolicy.maxAge,
|
|
487
|
+
suggestion: "Run preview again to generate a fresh receipt.",
|
|
488
|
+
}),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Drift checks
|
|
493
|
+
const driftResult = await performDriftChecks(receipt.driftKeys, {
|
|
494
|
+
driftPolicy: options.driftPolicy,
|
|
495
|
+
driftValues: options.driftValues,
|
|
496
|
+
resolveDriftValue: options.resolveDriftValue,
|
|
497
|
+
});
|
|
498
|
+
if (driftResult.error) {
|
|
499
|
+
return {
|
|
500
|
+
success: false,
|
|
501
|
+
receiptId: receipt.id,
|
|
502
|
+
transactions: [],
|
|
503
|
+
driftChecks: driftResult.driftChecks,
|
|
504
|
+
error: driftResult.error,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
// Build transactions for each planned action.
|
|
508
|
+
// Defer provider creation until we actually need it for EVM gas estimation.
|
|
509
|
+
const { chainId } = receipt.chainContext;
|
|
510
|
+
// Reject mismatched provider early — adapter context must use the receipt's
|
|
511
|
+
// chain so calldata (router addresses, token addresses) matches the preview.
|
|
512
|
+
if (options.provider && options.provider.chainId !== chainId) {
|
|
513
|
+
return {
|
|
514
|
+
success: false,
|
|
515
|
+
receiptId: receipt.id,
|
|
516
|
+
transactions: [],
|
|
517
|
+
driftChecks: driftResult.driftChecks,
|
|
518
|
+
error: createStructuredError("commit", "CHAIN_MISMATCH", `Provider chain ${options.provider.chainId} does not match receipt chain ${chainId}. ` +
|
|
519
|
+
`Provide a provider for chain ${chainId} or omit it to auto-create one.`),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
const registry = createVenueRegistry(options.adapters ?? []);
|
|
523
|
+
let lazyProvider = options.provider;
|
|
524
|
+
const getProvider = () => {
|
|
525
|
+
if (!lazyProvider) {
|
|
526
|
+
lazyProvider = createProvider(chainId, options.rpcUrl);
|
|
527
|
+
}
|
|
528
|
+
return lazyProvider;
|
|
529
|
+
};
|
|
530
|
+
const transactions = [];
|
|
531
|
+
for (const planned of receipt.plannedActions) {
|
|
532
|
+
try {
|
|
533
|
+
options.progressCallback?.(`Building transactions for step '${planned.stepId}'...`);
|
|
534
|
+
const builtTxs = await buildActionTransactions(planned.action, chainId, getProvider, options.walletAddress, receipt.chainContext.vault, registry);
|
|
535
|
+
transactions.push({
|
|
536
|
+
stepId: planned.stepId,
|
|
537
|
+
builtTransactions: builtTxs,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
542
|
+
if (planned.onFailure === "skip") {
|
|
543
|
+
options.progressCallback?.(`Skipping step '${planned.stepId}' during transaction build: ${message}`);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
success: false,
|
|
548
|
+
receiptId: receipt.id,
|
|
549
|
+
transactions,
|
|
550
|
+
driftChecks: driftResult.driftChecks,
|
|
551
|
+
error: createStructuredError("commit", "BUILD_TRANSACTIONS_FAILED", `Failed to build transactions for step '${planned.stepId}': ${message}`),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
success: true,
|
|
557
|
+
receiptId: receipt.id,
|
|
558
|
+
transactions,
|
|
559
|
+
driftChecks: driftResult.driftChecks,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
508
562
|
// =============================================================================
|
|
509
563
|
// EXECUTE (backward-compatible wrapper)
|
|
510
564
|
// =============================================================================
|
|
@@ -790,6 +844,54 @@ function registerIssuedReceipt(receipt) {
|
|
|
790
844
|
timestamp: receipt.timestamp,
|
|
791
845
|
});
|
|
792
846
|
}
|
|
847
|
+
// =============================================================================
|
|
848
|
+
// RECEIPT INTEGRITY (cross-process verification)
|
|
849
|
+
// =============================================================================
|
|
850
|
+
/**
|
|
851
|
+
* Deterministic serialization of the receipt fields that must not change
|
|
852
|
+
* between preview and buildTransactions. Used as the HMAC payload.
|
|
853
|
+
*/
|
|
854
|
+
function canonicalizeReceiptFields(receipt) {
|
|
855
|
+
const critical = {
|
|
856
|
+
id: receipt.id,
|
|
857
|
+
spellId: receipt.spellId,
|
|
858
|
+
chainId: receipt.chainContext.chainId,
|
|
859
|
+
vault: receipt.chainContext.vault,
|
|
860
|
+
timestamp: receipt.timestamp,
|
|
861
|
+
status: receipt.status,
|
|
862
|
+
plannedActions: receipt.plannedActions.map((pa) => ({
|
|
863
|
+
stepId: pa.stepId,
|
|
864
|
+
venue: pa.venue,
|
|
865
|
+
action: pa.action,
|
|
866
|
+
onFailure: pa.onFailure,
|
|
867
|
+
})),
|
|
868
|
+
};
|
|
869
|
+
return JSON.stringify(critical, (_key, value) => typeof value === "bigint" ? `__bigint__${value.toString()}` : value);
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Sign a receipt for cross-process integrity verification.
|
|
873
|
+
* Call this at preview time, persist the returned hex string alongside
|
|
874
|
+
* the receipt, and pass it as `receiptIntegrity` to buildTransactions().
|
|
875
|
+
*/
|
|
876
|
+
export function signReceipt(receipt, secret) {
|
|
877
|
+
const payload = canonicalizeReceiptFields(receipt);
|
|
878
|
+
return createHmac("sha256", secret).update(payload).digest("hex");
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Verify a receipt's HMAC integrity against the expected hash.
|
|
882
|
+
* Uses constant-time comparison to prevent timing attacks.
|
|
883
|
+
*/
|
|
884
|
+
function verifyReceiptIntegrity(receipt, secret, integrity) {
|
|
885
|
+
const expected = signReceipt(receipt, secret);
|
|
886
|
+
if (expected.length !== integrity.length)
|
|
887
|
+
return false;
|
|
888
|
+
try {
|
|
889
|
+
return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(integrity, "hex"));
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
793
895
|
function validateCommitReceipt(receipt) {
|
|
794
896
|
if (receipt.phase !== "preview") {
|
|
795
897
|
return createStructuredError("commit", "RECEIPT_INVALID_PHASE", `Receipt phase is '${receipt.phase}', expected 'preview'`);
|
|
@@ -812,7 +914,114 @@ function validateCommitReceipt(receipt) {
|
|
|
812
914
|
}
|
|
813
915
|
return undefined;
|
|
814
916
|
}
|
|
815
|
-
|
|
917
|
+
/**
|
|
918
|
+
* Receipt validation for buildTransactions().
|
|
919
|
+
*
|
|
920
|
+
* Same-process receipts (in issuedReceipts) are verified via the in-memory
|
|
921
|
+
* tamper check, exactly like commit().
|
|
922
|
+
*
|
|
923
|
+
* Cross-process receipts (not in issuedReceipts) require an HMAC integrity
|
|
924
|
+
* proof produced by signReceipt(). Without this, a caller could fabricate
|
|
925
|
+
* or modify plannedActions and obtain signable calldata for actions that
|
|
926
|
+
* were never previewed.
|
|
927
|
+
*
|
|
928
|
+
* We also check committedReceipts to prevent building calldata for a
|
|
929
|
+
* receipt that has already been committed.
|
|
930
|
+
*/
|
|
931
|
+
function validateBuildReceipt(receipt, integrity) {
|
|
932
|
+
if (receipt.phase !== "preview") {
|
|
933
|
+
return createStructuredError("commit", "RECEIPT_INVALID_PHASE", `Receipt phase is '${receipt.phase}', expected 'preview'`);
|
|
934
|
+
}
|
|
935
|
+
if (!receipt.id.startsWith("rcpt_")) {
|
|
936
|
+
return createStructuredError("commit", "RECEIPT_INVALID_ID", "Receipt ID must start with 'rcpt_'");
|
|
937
|
+
}
|
|
938
|
+
if (committedReceipts.has(receipt.id)) {
|
|
939
|
+
return createStructuredError("commit", "RECEIPT_ALREADY_COMMITTED", "Receipt has already been committed.");
|
|
940
|
+
}
|
|
941
|
+
// Same-process: verify against in-memory provenance
|
|
942
|
+
const issuedReceipt = issuedReceipts.get(receipt.id);
|
|
943
|
+
if (issuedReceipt) {
|
|
944
|
+
if (issuedReceipt.spellId !== receipt.spellId ||
|
|
945
|
+
issuedReceipt.chainId !== receipt.chainContext.chainId ||
|
|
946
|
+
issuedReceipt.vault !== receipt.chainContext.vault ||
|
|
947
|
+
issuedReceipt.timestamp !== receipt.timestamp) {
|
|
948
|
+
return createStructuredError("commit", "PREVIEW_RECEIPT_TAMPERED", "Receipt identity does not match the preview-generated artifact.");
|
|
949
|
+
}
|
|
950
|
+
return undefined;
|
|
951
|
+
}
|
|
952
|
+
// Cross-process: require HMAC integrity proof
|
|
953
|
+
if (!integrity.receiptSecret || !integrity.receiptIntegrity) {
|
|
954
|
+
return createStructuredError("commit", "RECEIPT_INTEGRITY_MISSING", "Cross-process receipts require receiptSecret and receiptIntegrity for verification. " +
|
|
955
|
+
"Use signReceipt() at preview time to generate the integrity hash.");
|
|
956
|
+
}
|
|
957
|
+
if (!verifyReceiptIntegrity(receipt, integrity.receiptSecret, integrity.receiptIntegrity)) {
|
|
958
|
+
return createStructuredError("commit", "RECEIPT_INTEGRITY_FAILED", "Receipt integrity verification failed. The receipt may have been modified.");
|
|
959
|
+
}
|
|
960
|
+
return undefined;
|
|
961
|
+
}
|
|
962
|
+
/** Run drift checks for a set of drift keys. Returns accumulated checks and optional error. */
|
|
963
|
+
async function performDriftChecks(driftKeys, options) {
|
|
964
|
+
const driftChecks = [];
|
|
965
|
+
for (const driftKey of driftKeys) {
|
|
966
|
+
if (options.driftPolicy?.maxAge) {
|
|
967
|
+
const keyAgeSec = Math.max(0, Math.floor((Date.now() - driftKey.timestamp) / 1000));
|
|
968
|
+
if (keyAgeSec > options.driftPolicy.maxAge) {
|
|
969
|
+
return {
|
|
970
|
+
driftChecks,
|
|
971
|
+
error: createStructuredError("commit", "DRIFT_KEY_STALE", `Drift key '${driftKey.field}' is stale (${keyAgeSec}s > ${options.driftPolicy.maxAge}s)`, {
|
|
972
|
+
constraint: "drift_key_freshness",
|
|
973
|
+
actual: keyAgeSec,
|
|
974
|
+
limit: options.driftPolicy.maxAge,
|
|
975
|
+
path: driftKey.field,
|
|
976
|
+
suggestion: "Run preview again to refresh drift keys.",
|
|
977
|
+
}),
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
let resolvedValue;
|
|
982
|
+
try {
|
|
983
|
+
resolvedValue = await resolveDriftValue(driftKey, options);
|
|
984
|
+
}
|
|
985
|
+
catch (error) {
|
|
986
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
987
|
+
return {
|
|
988
|
+
driftChecks,
|
|
989
|
+
error: createStructuredError("commit", "DRIFT_RESOLUTION_FAILED", `Failed to resolve drift value for '${driftKey.field}': ${message}`, {
|
|
990
|
+
constraint: "drift_keys",
|
|
991
|
+
path: driftKey.field,
|
|
992
|
+
}),
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
if (!resolvedValue.found && options.driftPolicy) {
|
|
996
|
+
return {
|
|
997
|
+
driftChecks,
|
|
998
|
+
error: createStructuredError("commit", "DRIFT_VALUE_MISSING", `Missing commit-time drift value for '${driftKey.field}'`, {
|
|
999
|
+
constraint: "drift_keys",
|
|
1000
|
+
path: driftKey.field,
|
|
1001
|
+
suggestion: "Provide driftValues for this key or configure resolveDriftValue to fetch commit-time values.",
|
|
1002
|
+
}),
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
const commitValue = resolvedValue.found ? resolvedValue.value : driftKey.previewValue;
|
|
1006
|
+
const driftResult = evaluateDriftKey(driftKey, commitValue, options.driftPolicy);
|
|
1007
|
+
driftChecks.push(driftResult);
|
|
1008
|
+
if (!driftResult.passed) {
|
|
1009
|
+
const tolerance = resolveToleranceBps(driftKey, options.driftPolicy);
|
|
1010
|
+
return {
|
|
1011
|
+
driftChecks,
|
|
1012
|
+
error: createStructuredError("commit", "DRIFT_EXCEEDED", `Drift exceeded for '${driftKey.field}'`, {
|
|
1013
|
+
constraint: "drift_policy",
|
|
1014
|
+
actual: driftResult.driftBps,
|
|
1015
|
+
limit: tolerance,
|
|
1016
|
+
path: driftKey.field,
|
|
1017
|
+
suggestion: "Run preview again or increase drift tolerance for this key class.",
|
|
1018
|
+
}),
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return { driftChecks };
|
|
1023
|
+
}
|
|
1024
|
+
async function resolveDriftValue(driftKey, options) {
|
|
816
1025
|
if (options.driftValues && Object.hasOwn(options.driftValues, driftKey.field)) {
|
|
817
1026
|
return { found: true, value: options.driftValues[driftKey.field] };
|
|
818
1027
|
}
|
|
@@ -824,6 +1033,73 @@ async function resolveCommitDriftValue(driftKey, options) {
|
|
|
824
1033
|
}
|
|
825
1034
|
return { found: false, value: undefined };
|
|
826
1035
|
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Build unsigned transactions for a single action.
|
|
1038
|
+
* Mirrors Executor.buildAction() for EVM actions without a wallet, and
|
|
1039
|
+
* rejects offchain adapters because they do not produce signable calldata.
|
|
1040
|
+
*
|
|
1041
|
+
* The provider is lazy so we never instantiate it for receipts that
|
|
1042
|
+
* contain only offchain or zero planned actions.
|
|
1043
|
+
*
|
|
1044
|
+
* Always uses the receipt's chainId for adapter context (not provider.chainId)
|
|
1045
|
+
* so calldata matches the previewed chain even if a mismatched provider
|
|
1046
|
+
* sneaks through.
|
|
1047
|
+
*/
|
|
1048
|
+
async function buildActionTransactions(action, chainId, getProvider, walletAddress, vault, registry) {
|
|
1049
|
+
const normalizeBuildResult = (result) => Array.isArray(result) ? result : [result];
|
|
1050
|
+
if (action.type === "custom") {
|
|
1051
|
+
const adapter = registry.get(action.venue);
|
|
1052
|
+
if (!adapter) {
|
|
1053
|
+
throw new Error(`Adapter '${action.venue}' is not registered for custom action '${action.op}'.`);
|
|
1054
|
+
}
|
|
1055
|
+
if (!adapter.meta.supportedChains.includes(chainId)) {
|
|
1056
|
+
throw new Error(`Adapter '${adapter.meta.name}' does not support chain ${chainId} for custom action '${action.op}'.`);
|
|
1057
|
+
}
|
|
1058
|
+
if (adapter.meta.executionType === "offchain") {
|
|
1059
|
+
throw new Error(`Adapter '${adapter.meta.name}' is offchain and does not produce signable transactions for custom action '${action.op}'. ` +
|
|
1060
|
+
`Use commit() instead for offchain execution.`);
|
|
1061
|
+
}
|
|
1062
|
+
const provider = getProvider();
|
|
1063
|
+
if (!adapter.buildAction) {
|
|
1064
|
+
throw new Error(`Adapter '${adapter.meta.name}' does not build custom EVM actions.`);
|
|
1065
|
+
}
|
|
1066
|
+
return normalizeBuildResult(await adapter.buildAction(action, {
|
|
1067
|
+
provider,
|
|
1068
|
+
walletAddress,
|
|
1069
|
+
vault,
|
|
1070
|
+
chainId,
|
|
1071
|
+
mode: "execute",
|
|
1072
|
+
}));
|
|
1073
|
+
}
|
|
1074
|
+
if ("venue" in action && action.venue) {
|
|
1075
|
+
const adapter = registry.get(action.venue);
|
|
1076
|
+
if (!adapter) {
|
|
1077
|
+
throw new Error(`Adapter '${action.venue}' is not registered`);
|
|
1078
|
+
}
|
|
1079
|
+
if (!adapter.meta.supportedChains.includes(chainId)) {
|
|
1080
|
+
throw new Error(`Adapter '${adapter.meta.name}' does not support chain ${chainId}.`);
|
|
1081
|
+
}
|
|
1082
|
+
if (adapter.meta.executionType === "offchain") {
|
|
1083
|
+
throw new Error(`Adapter '${adapter.meta.name}' is offchain and does not produce signable transactions. ` +
|
|
1084
|
+
`Use commit() instead for offchain execution.`);
|
|
1085
|
+
}
|
|
1086
|
+
const provider = getProvider();
|
|
1087
|
+
if (!adapter.buildAction) {
|
|
1088
|
+
throw new Error(`Adapter '${adapter.meta.name}' does not support EVM actions`);
|
|
1089
|
+
}
|
|
1090
|
+
return normalizeBuildResult(await adapter.buildAction(action, {
|
|
1091
|
+
provider,
|
|
1092
|
+
walletAddress,
|
|
1093
|
+
vault,
|
|
1094
|
+
chainId,
|
|
1095
|
+
mode: "execute",
|
|
1096
|
+
}));
|
|
1097
|
+
}
|
|
1098
|
+
// Built-in action types (transfer, approve, etc.)
|
|
1099
|
+
const provider = getProvider();
|
|
1100
|
+
const txBuilder = new TransactionBuilder(provider, walletAddress);
|
|
1101
|
+
return [await txBuilder.buildAction(action)];
|
|
1102
|
+
}
|
|
827
1103
|
function evaluateDriftKey(driftKey, commitValue, policy) {
|
|
828
1104
|
const tolerance = resolveToleranceBps(driftKey, policy);
|
|
829
1105
|
const numericPreview = toNumeric(driftKey.previewValue);
|