docs: Ledger Live integration, contract deploy learnings, NEXT_STEPS updates
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
- ADD_CHAIN138_TO_LEDGER_LIVE: Ledger form done; public code review repo bis-innovations/LedgerLive; init/push commands - CONTRACT_DEPLOYMENT_RUNBOOK: Chain 138 gas price 1 gwei, 36-addr check, TransactionMirror workaround - CONTRACT_*: AddressMapper, MirrorManager deployed 2026-02-12; 36-address on-chain check - NEXT_STEPS_FOR_YOU: Ledger done; steps completable now (no LAN); run-completable-tasks-from-anywhere - MASTER_INDEX, OPERATOR_OPTIONAL, SMART_CONTRACTS_INVENTORY_SIMPLE: updates - LEDGER_BLOCKCHAIN_INTEGRATION_COMPLETE: bis-innovations/LedgerLive reference Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
105
multi-chain-execution/src/api/admin-routes.ts
Normal file
105
multi-chain-execution/src/api/admin-routes.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Admin/ops API (protected). Policies, key rotation, circuit-breaker.
|
||||
* Auth: ADMIN_API_KEY (x-admin-key or admin_key query) or JWT later.
|
||||
* Audit: sends to dbis_core central audit when DBIS_CENTRAL_URL + ADMIN_CENTRAL_API_KEY set.
|
||||
*/
|
||||
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { setCircuitBreaker } from './observability.js';
|
||||
import { appendCentralAudit } from './central-audit.js';
|
||||
|
||||
const router: Router = Router();
|
||||
const ADMIN_API_KEY = process.env.ADMIN_API_KEY;
|
||||
|
||||
let policies: Record<string, unknown> = {};
|
||||
let lastKeyRotationAt: Date | null = null;
|
||||
|
||||
function getAdminSubject(req: Request): string {
|
||||
return (req.headers['x-admin-subject'] as string) || 'multi-chain-execution';
|
||||
}
|
||||
|
||||
function adminAuth(req: Request, res: Response, next: NextFunction): void {
|
||||
if (!ADMIN_API_KEY) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const key = req.headers['x-admin-key'] ?? req.query.admin_key;
|
||||
if (key !== ADMIN_API_KEY) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
router.use(adminAuth);
|
||||
|
||||
router.post('/v1/admin/policies', (req: Request, res: Response) => {
|
||||
const body = req.body as Record<string, unknown>;
|
||||
if (body && typeof body === 'object') {
|
||||
policies = { ...policies, ...body };
|
||||
}
|
||||
appendCentralAudit({
|
||||
employeeId: getAdminSubject(req),
|
||||
action: 'update_policies',
|
||||
permission: 'admin:action',
|
||||
resourceType: 'policies',
|
||||
metadata: body,
|
||||
ipAddress: req.ip || (req.headers['x-forwarded-for'] as string)?.split(',')[0],
|
||||
userAgent: req.get('user-agent') ?? undefined,
|
||||
}).catch(() => {});
|
||||
res.status(200).json({ message: 'Policy update accepted', policies });
|
||||
});
|
||||
|
||||
router.get('/v1/admin/policies', (_req: Request, res: Response) => {
|
||||
res.status(200).json({ policies });
|
||||
});
|
||||
|
||||
router.post('/v1/admin/keys/rotate', (req: Request, res: Response) => {
|
||||
lastKeyRotationAt = new Date();
|
||||
appendCentralAudit({
|
||||
employeeId: getAdminSubject(req),
|
||||
action: 'keys_rotate',
|
||||
permission: 'admin:action',
|
||||
resourceType: 'keys',
|
||||
ipAddress: req.ip || (req.headers['x-forwarded-for'] as string)?.split(',')[0],
|
||||
userAgent: req.get('user-agent') ?? undefined,
|
||||
}).catch(() => {});
|
||||
res.status(200).json({
|
||||
message: 'Key rotation initiated',
|
||||
rotated_at: lastKeyRotationAt.toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/v1/admin/keys/status', (_req: Request, res: Response) => {
|
||||
res.status(200).json({
|
||||
last_rotation: lastKeyRotationAt?.toISOString() ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/v1/admin/circuit-breaker/on', (req: Request, res: Response) => {
|
||||
setCircuitBreaker(true);
|
||||
appendCentralAudit({
|
||||
employeeId: getAdminSubject(req),
|
||||
action: 'circuit_breaker_on',
|
||||
permission: 'admin:action',
|
||||
resourceType: 'circuit_breaker',
|
||||
ipAddress: req.ip || (req.headers['x-forwarded-for'] as string)?.split(',')[0],
|
||||
userAgent: req.get('user-agent') ?? undefined,
|
||||
}).catch(() => {});
|
||||
res.status(200).json({ message: 'Circuit breaker forced open' });
|
||||
});
|
||||
|
||||
router.post('/v1/admin/circuit-breaker/off', (req: Request, res: Response) => {
|
||||
setCircuitBreaker(false);
|
||||
appendCentralAudit({
|
||||
employeeId: getAdminSubject(req),
|
||||
action: 'circuit_breaker_off',
|
||||
permission: 'admin:action',
|
||||
resourceType: 'circuit_breaker',
|
||||
ipAddress: req.ip || (req.headers['x-forwarded-for'] as string)?.split(',')[0],
|
||||
userAgent: req.get('user-agent') ?? undefined,
|
||||
}).catch(() => {});
|
||||
res.status(200).json({ message: 'Circuit breaker forced closed' });
|
||||
});
|
||||
|
||||
export default router;
|
||||
56
multi-chain-execution/src/api/central-audit.ts
Normal file
56
multi-chain-execution/src/api/central-audit.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Central audit client for multi-chain-execution admin actions.
|
||||
* Sends audit entries to dbis_core Admin Central API when DBIS_CENTRAL_URL and ADMIN_CENTRAL_API_KEY are set.
|
||||
*/
|
||||
|
||||
const DBIS_CENTRAL_URL = process.env.DBIS_CENTRAL_URL?.replace(/\/$/, '');
|
||||
const ADMIN_CENTRAL_API_KEY = process.env.ADMIN_CENTRAL_API_KEY;
|
||||
const SERVICE_NAME = 'multi_chain_execution';
|
||||
|
||||
function isConfigured(): boolean {
|
||||
return Boolean(DBIS_CENTRAL_URL && ADMIN_CENTRAL_API_KEY);
|
||||
}
|
||||
|
||||
export interface CentralAuditPayload {
|
||||
employeeId: string;
|
||||
action: string;
|
||||
permission: string;
|
||||
resourceType: string;
|
||||
resourceId?: string;
|
||||
outcome?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
}
|
||||
|
||||
export async function appendCentralAudit(payload: CentralAuditPayload): Promise<void> {
|
||||
if (!isConfigured()) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${DBIS_CENTRAL_URL}/api/admin/central/audit`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Admin-Central-Key': ADMIN_CENTRAL_API_KEY!,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
employeeId: payload.employeeId,
|
||||
action: payload.action,
|
||||
permission: payload.permission ?? 'admin:action',
|
||||
resourceType: payload.resourceType,
|
||||
resourceId: payload.resourceId,
|
||||
project: 'multi-chain-execution',
|
||||
service: SERVICE_NAME,
|
||||
outcome: payload.outcome ?? 'success',
|
||||
metadata: payload.metadata,
|
||||
ipAddress: payload.ipAddress,
|
||||
userAgent: payload.userAgent,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.warn(`[central-audit] POST failed: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[central-audit] append failed:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
}
|
||||
47
multi-chain-execution/src/api/execution-routes.ts
Normal file
47
multi-chain-execution/src/api/execution-routes.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Execution read API: GET /v1/executions/:id, GET /v1/tx/:chain_id/:tx_hash, GET /v1/audit/:executionId
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { getExecution } from '../eo/execution-orchestrator.js';
|
||||
import { getAdapter } from '../chain-adapters/get-adapter.js';
|
||||
import { getAudit } from '../audit/audit-store.js';
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.get('/v1/executions/:executionId', (req: Request, res: Response) => {
|
||||
const exec = getExecution(req.params.executionId);
|
||||
if (!exec) return res.status(404).json({ error: 'Execution not found' });
|
||||
res.json(exec);
|
||||
});
|
||||
|
||||
router.get('/v1/audit/:executionId', (req: Request, res: Response) => {
|
||||
const audit = getAudit(req.params.executionId);
|
||||
if (!audit) return res.status(404).json({ error: 'Audit not found' });
|
||||
res.json(audit);
|
||||
});
|
||||
|
||||
router.get('/v1/tx/:chainId/:txHash', async (req: Request, res: Response) => {
|
||||
const chainId = parseInt(req.params.chainId, 10);
|
||||
const txHash = req.params.txHash;
|
||||
if (isNaN(chainId) || !txHash) {
|
||||
return res.status(400).json({ error: 'Invalid chain_id or tx_hash' });
|
||||
}
|
||||
try {
|
||||
const adapter = getAdapter(chainId);
|
||||
const receipt = await adapter.getTransactionReceipt(txHash);
|
||||
if (!receipt) return res.status(404).json({ error: 'Transaction not found or pending' });
|
||||
const logs = await adapter.getLogs(
|
||||
Number(receipt.blockNumber),
|
||||
Number(receipt.blockNumber),
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
const txLogs = logs.filter((l) => l.transactionHash.toLowerCase() === txHash.toLowerCase());
|
||||
res.json({ receipt, logs: txLogs });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e instanceof Error ? e.message : 'RPC error' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
40
multi-chain-execution/src/api/intent-routes.ts
Normal file
40
multi-chain-execution/src/api/intent-routes.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { createIntent, getIntent, executeIntent } from '../eo/execution-orchestrator.js';
|
||||
import type { IntentRequest } from '../intent/types.js';
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.post('/v1/intents', (req: Request, res: Response) => {
|
||||
try {
|
||||
const body = req.body as IntentRequest;
|
||||
const intent = createIntent(body);
|
||||
res.status(201).json({
|
||||
intent_id: intent.intent_id,
|
||||
status: intent.status,
|
||||
planned_steps: intent.planned_steps,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: e instanceof Error ? e.message : 'Bad request' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/v1/intents/:intentId/execute', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { intentId } = req.params;
|
||||
const execution = await executeIntent(intentId);
|
||||
res.status(202).json({
|
||||
execution_id: execution.execution_id,
|
||||
submitted_txs: execution.submitted_txs,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: e instanceof Error ? e.message : 'Execute failed' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/v1/intents/:intentId', (req: Request, res: Response) => {
|
||||
const intent = getIntent(req.params.intentId);
|
||||
if (!intent) return res.status(404).json({ error: 'Intent not found' });
|
||||
res.json(intent);
|
||||
});
|
||||
|
||||
export default router;
|
||||
83
multi-chain-execution/src/api/mirror-routes.ts
Normal file
83
multi-chain-execution/src/api/mirror-routes.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Mirror API: POST /v1/mirror/commit, GET /v1/mirror/commits/:id
|
||||
* Proof API: GET /v1/mirror/proof?chain_id=&tx_hash=
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { buildCommitment, type CommitmentLeaf } from '../mirroring/merkle-commitment.js';
|
||||
import { saveCommit, getCommit, getProof } from '../mirroring/mirror-store.js';
|
||||
import type { StoredCommit } from '../mirroring/mirror-store.js';
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.post('/v1/mirror/commit', (req: Request, res: Response) => {
|
||||
try {
|
||||
const body = req.body as { chain_id: number; leaves: CommitmentLeaf[]; uri?: string };
|
||||
const { chain_id, leaves, uri = '' } = body;
|
||||
if (!leaves?.length || chain_id == null) {
|
||||
return res.status(400).json({ error: 'chain_id and leaves required' });
|
||||
}
|
||||
const result = buildCommitment(leaves, chain_id);
|
||||
const commitId = `commit-${uuidv4()}`;
|
||||
const leavesByTxHash = new Map<string, { leafIndex: number; leafData: unknown }>();
|
||||
leaves.forEach((leaf, i) => {
|
||||
leavesByTxHash.set(leaf.txHash.toLowerCase(), { leafIndex: i, leafData: leaf });
|
||||
});
|
||||
const stored: StoredCommit = {
|
||||
commitId,
|
||||
chainId: chain_id,
|
||||
startBlock: result.startBlock,
|
||||
endBlock: result.endBlock,
|
||||
root: result.root,
|
||||
uri,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
leafHashes: result.leafHashes,
|
||||
leavesByTxHash,
|
||||
publicChainTxHashes: [], // filled when MS posts to MirrorRegistry
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
saveCommit(stored);
|
||||
res.status(201).json({
|
||||
commit_id: commitId,
|
||||
root: result.root,
|
||||
start_block: result.startBlock,
|
||||
end_block: result.endBlock,
|
||||
chain_id: result.chainId,
|
||||
schema_version: result.schemaVersion,
|
||||
leaf_count: result.leafCount,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: e instanceof Error ? e.message : 'Bad request' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/v1/mirror/commits/:commitId', (req: Request, res: Response) => {
|
||||
const c = getCommit(req.params.commitId);
|
||||
if (!c) return res.status(404).json({ error: 'Commit not found' });
|
||||
res.json({
|
||||
commit_id: c.commitId,
|
||||
chain_id: c.chainId,
|
||||
start_block: c.startBlock,
|
||||
end_block: c.endBlock,
|
||||
root: c.root,
|
||||
uri: c.uri,
|
||||
timestamp: c.timestamp,
|
||||
leaf_count: c.leafHashes.length,
|
||||
public_chain_tx_hashes: c.publicChainTxHashes,
|
||||
created_at: c.createdAt,
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/v1/mirror/proof', (req: Request, res: Response) => {
|
||||
const chainId = parseInt(req.query.chain_id as string, 10);
|
||||
const txHash = req.query.tx_hash as string;
|
||||
if (isNaN(chainId) || !txHash) {
|
||||
return res.status(400).json({ error: 'chain_id and tx_hash query params required' });
|
||||
}
|
||||
const proof = getProof(chainId, txHash);
|
||||
if (!proof) return res.status(404).json({ error: 'No proof found for this tx' });
|
||||
res.json(proof);
|
||||
});
|
||||
|
||||
export default router;
|
||||
61
multi-chain-execution/src/api/observability.ts
Normal file
61
multi-chain-execution/src/api/observability.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Observability: circuit breaker, health, metrics.
|
||||
*/
|
||||
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
|
||||
let circuitOpen = false;
|
||||
let errorCount = 0;
|
||||
let lastErrorAt = 0;
|
||||
const ERROR_THRESHOLD = 10;
|
||||
const RESET_MS = 60_000;
|
||||
|
||||
/** Admin: force circuit breaker on or off. */
|
||||
export function setCircuitBreaker(open: boolean): void {
|
||||
circuitOpen = open;
|
||||
if (!open) errorCount = 0;
|
||||
}
|
||||
|
||||
export function circuitBreakerMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
if (circuitOpen) {
|
||||
if (Date.now() - lastErrorAt > RESET_MS) {
|
||||
circuitOpen = false;
|
||||
errorCount = 0;
|
||||
} else {
|
||||
res.status(503).json({ error: 'Circuit breaker open', retry_after: RESET_MS / 1000 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.on('finish', () => {
|
||||
if (res.statusCode >= 500) {
|
||||
errorCount++;
|
||||
lastErrorAt = Date.now();
|
||||
if (errorCount >= ERROR_THRESHOLD) circuitOpen = true;
|
||||
} else {
|
||||
errorCount = Math.max(0, errorCount - 1);
|
||||
}
|
||||
});
|
||||
next();
|
||||
}
|
||||
|
||||
const healthRouter: Router = Router();
|
||||
healthRouter.get('/v1/health', (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
status: circuitOpen ? 'degraded' : 'ok',
|
||||
circuit_breaker: circuitOpen ? 'open' : 'closed',
|
||||
error_count: errorCount,
|
||||
});
|
||||
});
|
||||
healthRouter.get('/v1/metrics', (_req: Request, res: Response) => {
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.send(
|
||||
`# HELP multi_chain_execution_errors Total 5xx errors\n` +
|
||||
`# TYPE multi_chain_execution_errors counter\n` +
|
||||
`multi_chain_execution_errors ${errorCount}\n` +
|
||||
`# HELP multi_chain_execution_circuit_open Circuit breaker open (1=open)\n` +
|
||||
`# TYPE multi_chain_execution_circuit_open gauge\n` +
|
||||
`multi_chain_execution_circuit_open ${circuitOpen ? 1 : 0}\n`
|
||||
);
|
||||
});
|
||||
|
||||
export const healthRoutes = healthRouter;
|
||||
89
multi-chain-execution/src/api/route-routes.ts
Normal file
89
multi-chain-execution/src/api/route-routes.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Chain138 -> Tezos USDtz route planning API
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
|
||||
const TEZOS_REGEX = /^(tz[1-4]|KT1)[1-9A-HJ-NP-Za-km-z]{33}$/;
|
||||
const CHAIN_138 = 138;
|
||||
const CHAIN_ALL_MAINNET = 651940;
|
||||
const CUSDC = '0xf22258f57794CC8E06237084b353Ab30fFfa640b';
|
||||
const AUSDC = '0xa95EeD79f84E6A0151eaEb9d441F9Ffd50e8e881';
|
||||
/** Active ETH→Tezos bridge (from bridge-capability-matrix) */
|
||||
const ETH_TO_TEZOS_PROVIDER = 'Wrap Protocol';
|
||||
|
||||
interface RoutePlanRequest {
|
||||
source_chain_id?: number;
|
||||
source_asset?: string;
|
||||
source_amount?: string;
|
||||
destination_tezos_address?: string;
|
||||
max_slippage_bps?: number;
|
||||
max_total_fees?: string;
|
||||
prefer_non_custodial?: boolean;
|
||||
}
|
||||
|
||||
interface RouteHop {
|
||||
chain: string;
|
||||
action: string;
|
||||
protocol: string;
|
||||
asset_in: string;
|
||||
amount_in: string;
|
||||
asset_out: string;
|
||||
min_amount_out: string;
|
||||
estimated_fees: string;
|
||||
}
|
||||
|
||||
interface RoutePlan {
|
||||
route_id: string;
|
||||
hops: RouteHop[];
|
||||
totalEstimatedFees: string;
|
||||
estimatedTimeSeconds: number;
|
||||
}
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.post('/v1/routes/chain138-to-usdtz', (req: Request, res: Response) => {
|
||||
try {
|
||||
const body = req.body as RoutePlanRequest;
|
||||
const sourceChainId = body.source_chain_id ?? 138;
|
||||
const sourceAsset = body.source_asset ?? CUSDC;
|
||||
const sourceAmount = body.source_amount ?? '0';
|
||||
const destAddr = body.destination_tezos_address ?? '';
|
||||
|
||||
if (sourceChainId !== CHAIN_138 && sourceChainId !== CHAIN_ALL_MAINNET) {
|
||||
return res.status(400).json({ valid: false, error: 'Only source_chain_id=138 (Chain138) or 651940 (ALL Mainnet) is supported' });
|
||||
}
|
||||
if (!destAddr.trim() || !TEZOS_REGEX.test(destAddr.trim())) {
|
||||
return res.status(400).json({ valid: false, error: 'Invalid destination_tezos_address' });
|
||||
}
|
||||
const amount = BigInt(sourceAmount);
|
||||
if (amount <= 0n) {
|
||||
return res.status(400).json({ valid: false, error: 'source_amount must be > 0' });
|
||||
}
|
||||
|
||||
const routeId = `route-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||
const isChain138 = sourceChainId === CHAIN_138;
|
||||
const sourceLabel = isChain138 ? 'CHAIN138' : 'ALL_MAINNET';
|
||||
const stableOut = isChain138 ? CUSDC : AUSDC;
|
||||
const srcBridge = isChain138 ? 'CCIP' : 'AlltraAdapter';
|
||||
const hops: RouteHop[] = [
|
||||
{ chain: sourceLabel, action: 'SWAP', protocol: isChain138 ? 'EnhancedSwapRouter' : 'AlltraDEX', asset_in: sourceAsset, amount_in: sourceAmount, asset_out: stableOut, min_amount_out: sourceAmount, estimated_fees: '0' },
|
||||
{ chain: sourceLabel, action: 'BRIDGE', protocol: srcBridge, asset_in: stableOut, amount_in: sourceAmount, asset_out: 'USDC', min_amount_out: sourceAmount, estimated_fees: '0' },
|
||||
{ chain: 'HUB_EVM', action: 'BRIDGE', protocol: ETH_TO_TEZOS_PROVIDER, asset_in: 'USDC', amount_in: sourceAmount, asset_out: 'USDC', min_amount_out: sourceAmount, estimated_fees: '0' },
|
||||
{ chain: 'TEZOS', action: 'SWAP', protocol: 'Plenty', asset_in: 'USDC', amount_in: sourceAmount, asset_out: 'USDtz', min_amount_out: sourceAmount, estimated_fees: '0' },
|
||||
];
|
||||
|
||||
const plan: RoutePlan = {
|
||||
route_id: routeId,
|
||||
hops,
|
||||
totalEstimatedFees: '0',
|
||||
estimatedTimeSeconds: 1800,
|
||||
};
|
||||
|
||||
res.status(200).json({ valid: true, routes: [plan] });
|
||||
} catch (e) {
|
||||
res.status(500).json({ valid: false, error: e instanceof Error ? e.message : 'Internal error' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
24
multi-chain-execution/src/api/server.ts
Normal file
24
multi-chain-execution/src/api/server.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import express from 'express';
|
||||
import intentRoutes from './intent-routes.js';
|
||||
import executionRoutes from './execution-routes.js';
|
||||
import mirrorRoutes from './mirror-routes.js';
|
||||
import adminRoutes from './admin-routes.js';
|
||||
import routeRoutes from './route-routes.js';
|
||||
import { circuitBreakerMiddleware, healthRoutes } from './observability.js';
|
||||
|
||||
const app: express.Express = express();
|
||||
app.use(express.json());
|
||||
app.use(circuitBreakerMiddleware);
|
||||
app.use(intentRoutes);
|
||||
app.use(executionRoutes);
|
||||
app.use(routeRoutes);
|
||||
app.use(mirrorRoutes);
|
||||
app.use(adminRoutes);
|
||||
app.use(healthRoutes);
|
||||
|
||||
const port = parseInt(process.env.PORT ?? '3001', 10);
|
||||
app.listen(port, () => {
|
||||
console.log(`Multi-chain execution API listening on port ${port}`);
|
||||
});
|
||||
|
||||
export { app };
|
||||
Reference in New Issue
Block a user