/** * Execution Orchestrator (EO). * Consumes intents, allocates nonces, submits txs via chain adapters, stores intent_id -> step -> tx_hash. */ import { validateAndPlan } from '../trpe/trpe.js'; import type { IntentRequest, Intent, Execution, ExecutionStepResult, PlannedStep } from '../intent/types.js'; import { nonceService } from '../nonce-service/nonce-service.js'; import { getAdapter } from '../chain-adapters/get-adapter.js'; import { recordAudit } from '../audit/audit-store.js'; import { v4 as uuidv4 } from 'uuid'; const intents = new Map(); const executions = new Map(); const intentIdByKey = new Map(); // idempotency_key -> intent_id export function createIntent(request: IntentRequest): Intent { const idempotencyKey = request.idempotency_key; if (idempotencyKey && intentIdByKey.has(idempotencyKey)) { const existing = intents.get(intentIdByKey.get(idempotencyKey)!); if (existing) return existing; } const result = validateAndPlan(request); if (!result.valid) { throw new Error(result.error ?? 'Validation failed'); } const intentId = `intent-${uuidv4()}`; const now = new Date().toISOString(); const intent: Intent = { intent_id: intentId, status: 'planned', request, planned_steps: result.planned_steps, created_at: now, updated_at: now, }; intents.set(intentId, intent); if (idempotencyKey) intentIdByKey.set(idempotencyKey, intentId); return intent; } export function getIntent(intentId: string): Intent | undefined { return intents.get(intentId); } /** * Execute planned steps: for MVP we simulate submission (no real wallet/signer). * In production EO would: * - Use real EVM/Tezos signers (wallet service or HSM) * - Replace placeholder txs with adapter.sendTransaction(signedTxHex) * - Plug in bridge-specific executors (IBridgeExecutor) for CCIP, Wrap, AlltraAdapter * - Implement retries, timeouts, idempotency keys per hop */ export async function executeIntent(intentId: string): Promise { const intent = intents.get(intentId); if (!intent) throw new Error('Intent not found'); if (intent.status !== 'planned') { const existing = executions.get(`${intentId}-exec`); if (existing) return existing; throw new Error(`Intent not in planned state: ${intent.status}`); } const executionId = `exec-${uuidv4()}`; const now = new Date().toISOString(); const steps: ExecutionStepResult[] = intent.planned_steps.map((s) => ({ step_index: s.step_index, step_type: s.step_type, chain_id: s.chain_id, status: 'pending' as const, })); const execution: Execution = { execution_id: executionId, intent_id: intentId, status: 'submitting', submitted_txs: [], steps, created_at: now, updated_at: now, }; executions.set(executionId, execution); executions.set(`${intentId}-exec`, execution); intent.status = 'executing'; intent.updated_at = now; // MVP: use placeholder when WALLET_ADDRESS not set. In production, set WALLET_ADDRESS and SIGNER_ENABLED=true. const wallet = process.env.WALLET_ADDRESS || '0x0000000000000000000000000000000000000001'; const usePlaceholder = !process.env.WALLET_ADDRESS; if (process.env.SIGNER_ENABLED === 'true' && usePlaceholder) { throw new Error('SIGNER_ENABLED=true requires WALLET_ADDRESS to be set'); } const lane = 'default'; for (const step of intent.planned_steps) { const adapter = getAdapter(step.chain_id); const nonce = nonceService.getNextNonce(step.chain_id, wallet, lane); // In production: build and sign tx, then adapter.sendTransaction(signedTxHex) const placeholderTxHash = `0x${Buffer.from(`${executionId}-${step.step_index}`).toString('hex').padEnd(64, '0')}`; nonceService.trackPending(step.chain_id, wallet, lane, placeholderTxHash); execution.submitted_txs.push({ step_index: step.step_index, chain_id: step.chain_id, tx_hash: placeholderTxHash }); const stepResult = execution.steps.find((s) => s.step_index === step.step_index); if (stepResult) { stepResult.tx_hash = placeholderTxHash; stepResult.status = 'submitted'; } } execution.status = 'completed'; execution.updated_at = new Date().toISOString(); intent.updated_at = execution.updated_at; intent.status = 'completed'; recordAudit({ intent_id: intentId, execution_id: executionId, created_at: execution.created_at, status: 'completed', hops: execution.steps.map((s) => ({ step_index: s.step_index, chain_id: s.chain_id, action: s.step_type, tx_hash: s.tx_hash, status: s.status ?? 'submitted', })), }); return execution; } export function getExecution(executionId: string): Execution | undefined { return executions.get(executionId); } export function getExecutionByIntent(intentId: string): Execution | undefined { return executions.get(`${intentId}-exec`); }