Files
proxmox/multi-chain-execution/src/eo/execution-orchestrator.ts

139 lines
4.8 KiB
TypeScript
Raw Normal View History

/**
* 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<string, Intent>();
const executions = new Map<string, Execution>();
const intentIdByKey = new Map<string, string>(); // 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<Execution> {
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`);
}