import fs from "fs"; import path from "path"; import type { ReleaseExecutionPlan } from "./executionPlan"; export interface ExternalCall { transactionId: string; stepId: string; to: string; value: string; data: string; operation: 0 | 1; method: string; args: unknown[]; executor: string; reason: string; } export interface ExternalExecutionGroup { groupId: string; executor: string; transactionIds: string[]; calls: ExternalCall[]; } export interface ExternalExecutionSegment { segmentId: string; transactionStartIndex: number; transactionIds: string[]; handoffBlock: number; groups: ExternalExecutionGroup[]; createdAt: string; } export interface ExternalExecutionPackage { version: 1; taskId: string; target: string; executionId: string; operator: string; executionPlanHash: string; segments: ExternalExecutionSegment[]; createdAt: string; updatedAt: string; } export class ExternalExecutionRequired extends Error { constructor( readonly stepId: string, readonly calls: ExternalCall[], ) { super(`External execution required for step ${stepId}`); this.name = "ExternalExecutionRequired"; } } const safeId = (value: string, label: string): string => { if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/.test(value)) { throw new Error(`${label} is invalid`); } return value; }; const normalizeAddress = (value: string, label: string): string => { if (!/^0x[0-9a-fA-F]{40}$/.test(value)) throw new Error(`${label} is invalid`); return value.toLowerCase(); }; const normalizeValue = (value: string, label: string): string => { if (!/^(?:0[xX][0-9a-fA-F]+|[0-9]+)$/.test(value)) { throw new Error(`${label} is invalid`); } return BigInt(value).toString(10); }; const normalizeData = (value: string, label: string): string => { if (!/^0x(?:[0-9a-fA-F]{2})*$/.test(value)) throw new Error(`${label} is invalid`); return value.toLowerCase(); }; const canonicalCall = (call: ExternalCall, index: number): ExternalCall => { if (call.operation !== 0 && call.operation !== 1) { throw new Error(`external call ${index} operation is invalid`); } return { transactionId: safeId(call.transactionId, `external call ${index} transactionId`), stepId: safeId(call.stepId, `external call ${index} stepId`), to: normalizeAddress(call.to, `external call ${index} to`), value: normalizeValue(call.value, `external call ${index} value`), data: normalizeData(call.data, `external call ${index} data`), operation: call.operation, method: String(call.method), args: call.args, executor: normalizeAddress(call.executor, `external call ${index} executor`), reason: String(call.reason), }; }; const groupAdjacentCalls = (calls: ExternalCall[]): ExternalExecutionGroup[] => { const groups: ExternalExecutionGroup[] = []; for (const call of calls) { const previous = groups.at(-1); if (previous && previous.executor === call.executor) { previous.transactionIds.push(call.transactionId); previous.calls.push(call); continue; } groups.push({ groupId: `group-${String(groups.length + 1).padStart(4, "0")}`, executor: call.executor, transactionIds: [call.transactionId], calls: [call], }); } return groups; }; const packagePath = (taskDir: string, target: string, executionId: string): string => { safeId(target, "external package target"); safeId(executionId, "external package executionId"); const resolvedTask = path.resolve(taskDir); const filePath = path.join( resolvedTask, "calldata", target, executionId, "external-execution.json", ); const relative = path.relative(resolvedTask, filePath); if (relative.startsWith("..") || path.isAbsolute(relative)) { throw new Error("external package path escapes the task directory"); } return filePath; }; const writeAtomic = (filePath: string, value: unknown): void => { fs.mkdirSync(path.dirname(filePath), { recursive: true }); const serialized = JSON.stringify(value, null, 2) + "\n"; const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`; try { fs.writeFileSync(temporary, serialized, { flag: "wx", mode: 0o600 }); fs.renameSync(temporary, filePath); } catch (error) { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); throw error; } }; const externalSegments = ( plan: ReleaseExecutionPlan, ): Array<{ index: number; transactions: ReleaseExecutionPlan["transactions"] }> => { const segments: Array<{ index: number; transactions: ReleaseExecutionPlan["transactions"] }> = []; for (const transaction of plan.transactions) { if (transaction.route !== "external") continue; const previous = segments.at(-1); if ( previous && previous.transactions.at(-1)!.transactionIndex + 1 === transaction.transactionIndex ) { previous.transactions.push(transaction); continue; } segments.push({ index: segments.length, transactions: [transaction] }); } return segments; }; const canonicalSegmentCalls = ( plan: ReleaseExecutionPlan, rawCalls: ExternalCall[], ): { segmentIndex: number; transactions: ReleaseExecutionPlan["transactions"]; calls: ExternalCall[] } => { if (rawCalls.length === 0) { throw new Error("external package calls must cover one consecutive external segment"); } const calls = rawCalls.map(canonicalCall); const segments = externalSegments(plan); const segmentIndex = segments.findIndex((segment) => ( segment.transactions.length === calls.length && segment.transactions.every((transaction, index) => ( transaction.transactionId === calls[index].transactionId )) )); if (segmentIndex < 0) { throw new Error("external package calls must cover one consecutive external segment"); } const transactions = segments[segmentIndex].transactions; for (const [index, transaction] of transactions.entries()) { const call = calls[index]; if ( call.stepId !== transaction.stepId || call.executor !== transaction.executor.toLowerCase() || call.to !== transaction.to.toLowerCase() || call.value !== BigInt(transaction.value).toString(10) || call.data !== transaction.data.toLowerCase() || call.operation !== transaction.operation ) { throw new Error( `external plan transaction ${transaction.transactionId} must match its segment call`, ); } } return { segmentIndex, transactions, calls }; }; const parseExistingPackage = ( filePath: string, plan: ReleaseExecutionPlan, ): ExternalExecutionPackage | undefined => { if (!fs.existsSync(filePath)) return undefined; let parsed: unknown; try { parsed = JSON.parse(fs.readFileSync(filePath, "utf8")); } catch (error) { throw new Error( `external execution package is invalid: ${error instanceof Error ? error.message : String(error)}`, ); } if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error("external execution package must be an object"); } const value = parsed as Record; if ( value.version !== 1 || value.taskId !== plan.taskId || value.target !== plan.target || value.executionId !== plan.executionId || value.operator !== plan.operator || value.executionPlanHash !== plan.planHash || !Array.isArray(value.segments) || typeof value.createdAt !== "string" || typeof value.updatedAt !== "string" ) { throw new Error("external execution package does not match the execution plan"); } const segments = externalSegments(plan); for (const [index, rawSegment] of value.segments.entries()) { if (!rawSegment || typeof rawSegment !== "object" || Array.isArray(rawSegment)) { throw new Error(`external execution segment ${index} is invalid`); } const segment = rawSegment as ExternalExecutionSegment; const expected = segments[index]?.transactions; const calls = Array.isArray(segment.groups) ? segment.groups.flatMap((group) => group.calls) : []; if ( !expected || segment.segmentId !== `segment-${String(index + 1).padStart(4, "0")}` || segment.transactionStartIndex !== expected[0].transactionIndex || !Array.isArray(segment.transactionIds) || JSON.stringify(segment.transactionIds) !== JSON.stringify( expected.map((transaction) => transaction.transactionId), ) || !Number.isSafeInteger(segment.handoffBlock) || segment.handoffBlock < 0 || calls.length !== expected.length ) { throw new Error(`external execution segment ${index} does not match the execution plan`); } canonicalSegmentCalls(plan, calls); } return value as unknown as ExternalExecutionPackage; }; export const loadExternalExecutionPackage = (input: { taskDir: string; plan: ReleaseExecutionPlan; }): { filePath: string; package: ExternalExecutionPackage } | undefined => { const filePath = packagePath(input.taskDir, input.plan.target, input.plan.executionId); const existing = parseExistingPackage(filePath, input.plan); return existing ? { filePath, package: existing } : undefined; }; export const writeExternalExecutionPackage = (input: { taskDir: string; plan: ReleaseExecutionPlan; handoffBlock: number; calls: ExternalCall[]; now?: () => string; }): { filePath: string; package: ExternalExecutionPackage } => { if (!Number.isSafeInteger(input.handoffBlock) || input.handoffBlock < 0) { throw new Error("external package handoffBlock must be a nonnegative safe integer"); } if (!input.plan.transactions.some((transaction) => transaction.route === "external")) { throw new Error("external execution package requires external plan transactions"); } const { segmentIndex, transactions, calls } = canonicalSegmentCalls(input.plan, input.calls); const target = safeId(input.plan.target, "external package target"); const executionId = safeId(input.plan.executionId, "external package executionId"); const filePath = packagePath(input.taskDir, target, executionId); const existing = parseExistingPackage(filePath, input.plan); if (existing?.segments[segmentIndex]) { return { filePath, package: existing }; } if ((existing?.segments.length ?? 0) !== segmentIndex) { throw new Error("external execution segments must be appended in plan order"); } const now = (input.now || (() => new Date().toISOString()))(); const segment: ExternalExecutionSegment = { segmentId: `segment-${String(segmentIndex + 1).padStart(4, "0")}`, transactionStartIndex: transactions[0].transactionIndex, transactionIds: transactions.map((transaction) => transaction.transactionId), handoffBlock: input.handoffBlock, groups: groupAdjacentCalls(calls), createdAt: now, }; const output: ExternalExecutionPackage = existing ? { ...existing, segments: [...existing.segments, segment], updatedAt: now } : { version: 1, taskId: safeId(input.plan.taskId, "external package taskId"), target, executionId, operator: normalizeAddress(input.plan.operator, "external package operator"), executionPlanHash: input.plan.planHash, segments: [segment], createdAt: now, updatedAt: now, }; writeAtomic(filePath, output); return { filePath, package: output }; };