docs: Ledger Live integration, contract deploy learnings, NEXT_STEPS updates
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:
defiQUG
2026-02-12 15:46:57 -08:00
parent cc8dcaf356
commit fbda1b4beb
5114 changed files with 498901 additions and 4567 deletions

View 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;

View 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);
}
}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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 };

View File

@@ -0,0 +1,47 @@
/**
* Audit store for Chain138->Tezos USDtz executions.
* Stores pre-trade quotes, tx hashes per hop, bridge message IDs, final delivered amount.
*/
export interface ExecutionAudit {
intent_id: string;
execution_id: string;
route_id?: string;
created_at: string;
status: 'planned' | 'quoted' | 'executing' | 'completed' | 'failed';
pre_trade_quote?: {
amount_in: string;
expected_out: string;
fees: string;
quoted_at: string;
};
hops: Array<{
step_index: number;
chain_id: number;
action: string;
tx_hash?: string;
block_number?: string;
bridge_message_id?: string;
status: string;
}>;
final_delivered?: {
amount: string;
asset: string;
destination: string;
tx_hash?: string;
};
}
const store = new Map<string, ExecutionAudit>();
export function recordAudit(audit: ExecutionAudit): void {
store.set(audit.execution_id, audit);
}
export function getAudit(executionId: string): ExecutionAudit | undefined {
return store.get(executionId);
}
export function getAuditsByIntent(intentId: string): ExecutionAudit[] {
return Array.from(store.values()).filter((a) => a.intent_id === intentId);
}

View File

@@ -0,0 +1,17 @@
import { BaseChainAdapter } from './base-adapter.js';
import { getChainConfig } from './config.js';
/**
* Chain adapter for DBIS Chain ID 138.
*/
export class ChainAdapter138 extends BaseChainAdapter {
constructor(rpcUrls?: string[]) {
const config = getChainConfig(138);
if (!config) throw new Error('Chain 138 config not found');
super(138, rpcUrls ?? config.rpcUrls);
}
}
export function createAdapter138(rpcUrls?: string[]): ChainAdapter138 {
return new ChainAdapter138(rpcUrls);
}

View File

@@ -0,0 +1,17 @@
import { BaseChainAdapter } from './base-adapter.js';
import { getChainConfig } from './config.js';
/**
* Chain adapter for ALL Mainnet (651940).
*/
export class ChainAdapter651940 extends BaseChainAdapter {
constructor(rpcUrls?: string[]) {
const config = getChainConfig(651940);
if (!config) throw new Error('Chain 651940 config not found');
super(651940, rpcUrls ?? config.rpcUrls);
}
}
export function createAdapter651940(rpcUrls?: string[]): ChainAdapter651940 {
return new ChainAdapter651940(rpcUrls);
}

View File

@@ -0,0 +1,24 @@
import { BaseChainAdapter } from './base-adapter.js';
import { getChainConfig, getSupportedChainIds } from './config.js';
/**
* Generic public mainnet adapter (Ethereum, Arbitrum, Base, Polygon, BSC).
*/
export class ChainAdapterPublic extends BaseChainAdapter {
constructor(chainId: number, rpcUrls?: string[]) {
const config = getChainConfig(chainId);
if (!config) throw new Error(`Unsupported public chainId: ${chainId}`);
if (chainId === 138 || chainId === 651940) {
throw new Error('Use ChainAdapter138 or ChainAdapter651940 for private chains');
}
super(chainId, rpcUrls ?? config.rpcUrls);
}
}
export function createAdapterPublic(chainId: number, rpcUrls?: string[]): ChainAdapterPublic {
return new ChainAdapterPublic(chainId, rpcUrls);
}
export function getPublicChainIds(): number[] {
return getSupportedChainIds().filter((id) => id !== 138 && id !== 651940);
}

View File

@@ -0,0 +1,140 @@
/**
* Chain adapter for Tezos mainnet (chainId 1729).
* Uses TzKT API for read operations. sendTransaction requires Taquito/injection for production.
*/
import type {
IChainAdapter,
ChainAdapterConfig,
NormalizedReceipt,
NormalizedLog,
SendTransactionResult,
} from './types.js';
import { getChainConfig, TEZOS_CHAIN_ID } from './config.js';
const TZKt_BASE = 'https://api.tzkt.io';
export class TezosChainAdapter implements IChainAdapter {
private config: ChainAdapterConfig;
private baseUrl: string;
constructor(rpcUrls?: string[]) {
const cfg = getChainConfig(TEZOS_CHAIN_ID);
if (!cfg) throw new Error('Tezos chain config not found');
this.config = rpcUrls?.length ? { ...cfg, rpcUrls } : cfg;
this.baseUrl = this.config.rpcUrls[0].replace(/\/$/, '');
}
getChainId(): number {
return this.config.chainId;
}
getConfig(): ChainAdapterConfig {
return this.config;
}
async getBlockNumber(): Promise<number> {
const res = await fetch(`${this.baseUrl}/v1/blocks/count`);
if (!res.ok) throw new Error(`TzKT blocks/count failed: ${res.status}`);
const count = await res.json();
return Number(count);
}
async getBlock(blockNumber: number): Promise<{ number: number; hash: string; parentHash: string; timestamp: number } | null> {
const res = await fetch(`${this.baseUrl}/v1/blocks/${blockNumber}`);
if (!res.ok) return null;
const b = await res.json();
return {
number: b.level,
hash: b.hash ?? '',
parentHash: b.previousHash ?? '',
timestamp: new Date(b.timestamp).getTime() / 1000,
};
}
async sendTransaction(signedTxHex: string): Promise<SendTransactionResult> {
const hex = signedTxHex.startsWith('0x') ? signedTxHex.slice(2) : signedTxHex;
const bytes = Buffer.from(hex, 'hex');
const rpcUrl = process.env.TEZOS_RPC_INJECT_URL ?? 'https://mainnet.api.tez.ie';
const res = await fetch(`${rpcUrl}/injection/operation`, {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: bytes,
signal: AbortSignal.timeout(15000),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`Tezos injection failed: ${res.status} ${err}`);
}
const opHash = await res.text();
return { hash: opHash.trim(), from: '', nonce: 0 };
}
async getTransactionReceipt(txHash: string): Promise<NormalizedReceipt | null> {
const res = await fetch(`${this.baseUrl}/v1/operations/transactions/${txHash}`);
if (!res.ok) return null;
const op = await res.json();
if (Array.isArray(op)) {
const t = op[0];
if (!t) return null;
return {
chainId: this.config.chainId,
transactionHash: t.hash,
blockNumber: BigInt(t.level ?? 0),
blockHash: t.block ?? '',
transactionIndex: 0,
from: t.sender?.address ?? '',
to: t.target?.address ?? null,
gasUsed: BigInt(t.gasUsed ?? 0),
cumulativeGasUsed: BigInt(t.gasUsed ?? 0),
contractAddress: t.target?.address ?? null,
logsBloom: '',
status: t.status === 'applied' ? 1 : 0,
root: null,
};
}
return {
chainId: this.config.chainId,
transactionHash: op.hash,
blockNumber: BigInt(op.level ?? 0),
blockHash: op.block ?? '',
transactionIndex: 0,
from: op.sender?.address ?? '',
to: op.target?.address ?? null,
gasUsed: BigInt(op.gasUsed ?? 0),
cumulativeGasUsed: BigInt(op.gasUsed ?? 0),
contractAddress: op.target?.address ?? null,
logsBloom: '',
status: op.status === 'applied' ? 1 : 0,
root: null,
};
}
async getLogs(
_fromBlock: number,
_toBlock: number,
_address?: string,
_topics?: string[]
): Promise<NormalizedLog[]> {
return [];
}
async detectReorg(blockNumber: number, expectedBlockHash: string): Promise<boolean> {
const block = await this.getBlock(blockNumber);
if (!block) return true;
return block.hash !== expectedBlockHash;
}
async healthCheck(): Promise<boolean> {
try {
await fetch(`${this.baseUrl}/v1/blocks/head`);
return true;
} catch {
return false;
}
}
}
export function createAdapterTezos(rpcUrls?: string[]): TezosChainAdapter {
return new TezosChainAdapter(rpcUrls);
}

View File

@@ -0,0 +1,156 @@
/**
* Base chain adapter: RPC abstraction, receipt/log fetch, reorg detection, fallback RPC.
*/
import { JsonRpcProvider, TransactionReceipt, Log } from 'ethers';
import type { ChainAdapterConfig, NormalizedReceipt, NormalizedLog, IChainAdapter } from './types.js';
import { getChainConfig } from './config.js';
function toHex(n: bigint): string {
return '0x' + n.toString(16);
}
export abstract class BaseChainAdapter implements IChainAdapter {
protected provider: JsonRpcProvider;
protected config: ChainAdapterConfig;
private rpcIndex = 0;
constructor(chainId: number, rpcUrls?: string[]) {
const cfg = getChainConfig(chainId);
if (!cfg) throw new Error(`Unknown chainId: ${chainId}`);
this.config = rpcUrls?.length ? { ...cfg, rpcUrls } : cfg;
this.provider = new JsonRpcProvider(this.config.rpcUrls[0]);
}
getChainId(): number {
return this.config.chainId;
}
getConfig(): ChainAdapterConfig {
return this.config;
}
protected getRpcUrl(): string {
return this.config.rpcUrls[this.rpcIndex % this.config.rpcUrls.length];
}
protected async switchRpc(): Promise<void> {
if (this.config.rpcUrls.length <= 1) return;
this.rpcIndex++;
this.provider = new JsonRpcProvider(this.getRpcUrl());
}
async getBlockNumber(): Promise<number> {
const n = await this.provider.getBlockNumber();
return n;
}
async getBlock(blockNumber: number): Promise<{ number: number; hash: string; parentHash: string; timestamp: number } | null> {
try {
const block = await this.provider.getBlock(blockNumber);
if (!block) return null;
return {
number: block.number,
hash: block.hash ?? '',
parentHash: block.parentHash ?? '',
timestamp: block.timestamp,
};
} catch {
return null;
}
}
async sendTransaction(signedTxHex: string): Promise<{ hash: string; from: string; nonce: number }> {
const tx = await this.provider.broadcastTransaction(signedTxHex);
return {
hash: tx.hash,
from: tx.from ?? '',
nonce: tx.nonce,
};
}
async getTransactionReceipt(txHash: string): Promise<NormalizedReceipt | null> {
try {
const receipt = await this.provider.getTransactionReceipt(txHash);
if (!receipt) return null;
return this.normalizeReceipt(receipt);
} catch {
return null;
}
}
protected normalizeReceipt(receipt: TransactionReceipt): NormalizedReceipt {
return {
chainId: this.config.chainId,
transactionHash: receipt.hash,
blockNumber: BigInt(receipt.blockNumber),
blockHash: receipt.blockHash ?? '',
transactionIndex: receipt.index,
from: receipt.from,
to: receipt.to ?? null,
gasUsed: BigInt(receipt.gasUsed.toString()),
cumulativeGasUsed: BigInt(receipt.cumulativeGasUsed.toString()),
contractAddress: receipt.contractAddress ?? null,
logsBloom: receipt.logsBloom ?? '',
status: receipt.status === 1 ? 1 : 0,
root: receipt.root ?? null,
};
}
async getLogs(
fromBlock: number,
toBlock: number,
address?: string,
topics?: string[]
): Promise<NormalizedLog[]> {
const filter: { fromBlock: number; toBlock: number; address?: string; topics?: string[] } = {
fromBlock,
toBlock,
};
if (address) filter.address = address;
if (topics?.length) filter.topics = topics as `0x${string}`[];
const logs = await this.provider.getLogs(filter);
return logs.map((log) => this.normalizeLog(log));
}
protected normalizeLog(log: Log): NormalizedLog {
const topics = log.topics as string[];
return {
chainId: this.config.chainId,
transactionHash: log.transactionHash,
blockNumber: BigInt(log.blockNumber),
blockHash: log.blockHash ?? '',
logIndex: log.index,
address: log.address,
topic0: topics[0] ?? null,
topic1: topics[1] ?? null,
topic2: topics[2] ?? null,
topic3: topics[3] ?? null,
data: log.data,
};
}
async detectReorg(blockNumber: number, expectedBlockHash: string): Promise<boolean> {
const block = await this.getBlock(blockNumber);
if (!block) return true;
return block.hash.toLowerCase() !== expectedBlockHash.toLowerCase();
}
async healthCheck(): Promise<boolean> {
try {
await this.provider.getBlockNumber();
return true;
} catch {
if (this.config.rpcUrls.length > 1) {
await this.switchRpc();
try {
await this.provider.getBlockNumber();
return true;
} catch {
return false;
}
}
return false;
}
}
}

View File

@@ -0,0 +1,90 @@
/**
* Chain config for CA-138, CA-651940, and public mainnets.
* Single source of truth for chainId, RPC, confirmations, reorg window.
*/
import type { ChainAdapterConfig } from './types.js';
const CHAIN_138_RPC =
process.env.CHAIN_138_RPC_URL ?? process.env.CHAIN138_RPC_URL ?? 'https://rpc-http-pub.d-bis.org';
const CHAIN_651940_RPC =
process.env.CHAIN_651940_RPC_URL ?? 'https://mainnet-rpc.alltra.global';
const ETHEREUM_RPC = process.env.ETHEREUM_RPC_URL ?? 'https://eth.llamarpc.com';
const ARBITRUM_RPC = process.env.ARBITRUM_RPC_URL ?? 'https://arb1.arbitrum.io/rpc';
const BASE_RPC = process.env.BASE_RPC_URL ?? 'https://mainnet.base.org';
const POLYGON_RPC = process.env.POLYGON_RPC_URL ?? 'https://polygon-rpc.com';
const BSC_RPC = process.env.BSC_RPC_URL ?? 'https://bsc-dataseed.binance.org';
/** Tezos mainnet - chainId 1729 (Tezos founding year). Uses TzKT API for read operations. */
const TEZOS_RPC =
process.env.TEZOS_RPC_URL ?? process.env.TEZOS_TZKT_URL ?? 'https://api.tzkt.io';
/** Chain ID for Tezos mainnet (non-EVM) */
export const TEZOS_CHAIN_ID = 1729;
export const CHAIN_ADAPTER_CONFIGS: Record<number, ChainAdapterConfig> = {
138: {
chainId: 138,
rpcUrls: [CHAIN_138_RPC],
confirmations: 20,
chainKey: 'chainid-138',
reorgWindowBlocks: 20,
},
651940: {
chainId: 651940,
rpcUrls: [CHAIN_651940_RPC],
confirmations: 20,
chainKey: 'all-mainnet',
reorgWindowBlocks: 20,
},
1: {
chainId: 1,
rpcUrls: [ETHEREUM_RPC],
confirmations: 32,
chainKey: 'ethereum-mainnet',
reorgWindowBlocks: 64,
},
42161: {
chainId: 42161,
rpcUrls: [ARBITRUM_RPC],
confirmations: 20,
chainKey: 'arbitrum-one',
reorgWindowBlocks: 40,
},
8453: {
chainId: 8453,
rpcUrls: [BASE_RPC],
confirmations: 10,
chainKey: 'base',
reorgWindowBlocks: 20,
},
137: {
chainId: 137,
rpcUrls: [POLYGON_RPC],
confirmations: 128,
chainKey: 'polygon',
reorgWindowBlocks: 128,
},
56: {
chainId: 56,
rpcUrls: [BSC_RPC],
confirmations: 15,
chainKey: 'bsc',
reorgWindowBlocks: 30,
},
[TEZOS_CHAIN_ID]: {
chainId: TEZOS_CHAIN_ID,
rpcUrls: [TEZOS_RPC],
confirmations: 2,
chainKey: 'tezos-mainnet',
reorgWindowBlocks: 2,
},
};
export function getChainConfig(chainId: number): ChainAdapterConfig | undefined {
return CHAIN_ADAPTER_CONFIGS[chainId];
}
export function getSupportedChainIds(): number[] {
return Object.keys(CHAIN_ADAPTER_CONFIGS).map(Number);
}

View File

@@ -0,0 +1,13 @@
import type { IChainAdapter } from './types.js';
import { createAdapter138 } from './adapter-138.js';
import { createAdapter651940 } from './adapter-651940.js';
import { createAdapterPublic } from './adapter-public.js';
import { createAdapterTezos } from './adapter-tezos.js';
import { TEZOS_CHAIN_ID } from './config.js';
export function getAdapter(chainId: number): IChainAdapter {
if (chainId === 138) return createAdapter138();
if (chainId === 651940) return createAdapter651940();
if (chainId === TEZOS_CHAIN_ID) return createAdapterTezos();
return createAdapterPublic(chainId);
}

View File

@@ -0,0 +1,8 @@
export * from './types.js';
export * from './config.js';
export * from './base-adapter.js';
export * from './adapter-138.js';
export * from './adapter-651940.js';
export * from './adapter-public.js';
export * from './adapter-tezos.js';
export * from './get-adapter.js';

View File

@@ -0,0 +1,78 @@
/**
* Chain adapter types for CA-138, CA-651940, CA-publicN.
* Used by EO, EII, and Mirroring Service.
*/
export interface ChainAdapterConfig {
chainId: number;
rpcUrls: string[];
confirmations: number;
chainKey: string;
/** Block numbers below this are considered finalized (reorg window). */
reorgWindowBlocks?: number;
}
export interface NormalizedReceipt {
chainId: number;
transactionHash: string;
blockNumber: bigint;
blockHash: string;
transactionIndex: number;
from: string;
to: string | null;
gasUsed: bigint;
cumulativeGasUsed: bigint;
contractAddress: string | null;
logsBloom: string;
status: number;
root: string | null;
}
export interface NormalizedLog {
chainId: number;
transactionHash: string;
blockNumber: bigint;
blockHash: string;
logIndex: number;
address: string;
topic0: string | null;
topic1: string | null;
topic2: string | null;
topic3: string | null;
data: string;
}
export interface SendTransactionResult {
hash: string;
from: string;
nonce: number;
}
export interface IChainAdapter {
getChainId(): number;
getConfig(): ChainAdapterConfig;
/** Get current block number. */
getBlockNumber(): Promise<number>;
/** Get block by number; returns null if not found. */
getBlock(blockNumber: number): Promise<{ number: number; hash: string; parentHash: string; timestamp: number } | null>;
/** Send raw signed transaction (hex). Returns tx hash. */
sendTransaction(signedTxHex: string): Promise<SendTransactionResult>;
/** Get transaction receipt; returns null if pending/unknown. */
getTransactionReceipt(txHash: string): Promise<NormalizedReceipt | null>;
/** Get logs for block range (inclusive). */
getLogs(fromBlock: number, toBlock: number, address?: string, topics?: string[]): Promise<NormalizedLog[]>;
/**
* Reorg detection: fetch block by number and compare hash to expected.
* Returns true if chain has reorged (hash mismatch).
*/
detectReorg(blockNumber: number, expectedBlockHash: string): Promise<boolean>;
/** Health check: can we reach RPC? */
healthCheck(): Promise<boolean>;
}

View File

@@ -0,0 +1,138 @@
/**
* 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`);
}

View File

@@ -0,0 +1,8 @@
export * from './chain-adapters/index.js';
export * from './intent/types.js';
export { createIntent, getIntent, executeIntent, getExecution, getExecutionByIntent } from './eo/execution-orchestrator.js';
export { validateAndPlan } from './trpe/trpe.js';
export { nonceService } from './nonce-service/nonce-service.js';
export { buildCommitment, buildMerkleRoot, buildMerkleProof, hashLeaf, type CommitmentLeaf, type CommitmentResult } from './mirroring/merkle-commitment.js';
export { saveCommit, getCommit, getProof } from './mirroring/mirror-store.js';
export type { StoredCommit } from './mirroring/mirror-store.js';

View File

@@ -0,0 +1,56 @@
export type StepType = 'transfer' | 'swap' | 'bridge' | 'message_send' | 'message_receive' | 'mint' | 'burn';
export interface IntentRequest {
type: string;
chain_from: number;
chain_to: number;
asset_in: string;
asset_out: string;
amount: string;
max_slippage_bps?: number;
ttl_ms?: number;
metadata?: Record<string, unknown>;
idempotency_key?: string;
destination_tezos_address?: string;
max_total_fees?: string;
prefer_non_custodial?: boolean;
require_audit_proof?: boolean;
/** When chain_to is Tezos (1729), optional route plan to map to execution steps */
route_plan?: { hops: Array<{ chain: string; action: string; chain_id?: number }> };
}
export interface PlannedStep {
step_index: number;
step_type: StepType;
chain_id: number;
preconditions?: string[];
postconditions?: string[];
}
export interface Intent {
intent_id: string;
status: 'created' | 'planned' | 'executing' | 'completed' | 'failed';
request: IntentRequest;
planned_steps: PlannedStep[];
created_at: string;
updated_at: string;
}
export interface ExecutionStepResult {
step_index: number;
step_type: StepType;
chain_id: number;
tx_hash?: string;
status: 'pending' | 'submitted' | 'confirmed' | 'finalized' | 'failed';
error?: string;
}
export interface Execution {
execution_id: string;
intent_id: string;
status: 'pending' | 'submitting' | 'completed' | 'failed';
submitted_txs: { step_index: number; chain_id: number; tx_hash: string }[];
steps: ExecutionStepResult[];
created_at: string;
updated_at: string;
}

View File

@@ -0,0 +1,4 @@
/**
* Entry point for multi-chain execution API server.
*/
import './api/server.js';

View File

@@ -0,0 +1,108 @@
/**
* Merkle commitment builder for mirroring.
* Builds tree over leaves: tx_hash, block_number, receipt_root_or_logs_bloom, payload_hash, sal_journal_hash.
* Output: root, range [startBlock, endBlock], chain_id, schema_version.
*/
import { keccak256 } from 'ethers';
export interface CommitmentLeaf {
chainId: number;
txHash: string;
blockNumber: bigint;
receiptRootOrLogsBloom: string;
normalizedEventPayloadHash: string;
salJournalEntryHash: string | null;
}
const SCHEMA_VERSION = 1;
/**
* Canonical leaf hash for commitment (per event-schema-v1).
*/
export function hashLeaf(leaf: CommitmentLeaf): string {
const payload = [
leaf.chainId.toString(16),
leaf.txHash.toLowerCase(),
leaf.blockNumber.toString(16),
leaf.receiptRootOrLogsBloom.toLowerCase(),
leaf.normalizedEventPayloadHash.toLowerCase(),
leaf.salJournalEntryHash?.toLowerCase() ?? '',
].join('|');
return keccak256(Buffer.from(payload, 'utf-8'));
}
/**
* Build Merkle root from leaf hashes (simple pair-wise hash up).
*/
export function buildMerkleRoot(leafHashes: string[]): string {
if (leafHashes.length === 0) {
return keccak256(Buffer.from('empty'));
}
let level = leafHashes.map((h) => h.toLowerCase());
while (level.length > 1) {
const next: string[] = [];
for (let i = 0; i < level.length; i += 2) {
const left = level[i];
const right = i + 1 < level.length ? level[i + 1] : left;
next.push(keccak256(Buffer.from(left.slice(2) + right.slice(2), 'hex')));
}
level = next;
}
return level[0];
}
/**
* Build Merkle proof for a leaf index.
*/
export function buildMerkleProof(leafHashes: string[], index: number): string[] {
if (index < 0 || index >= leafHashes.length) return [];
const proof: string[] = [];
let level = leafHashes.map((h) => h.toLowerCase());
let idx = index;
while (level.length > 1) {
const next: string[] = [];
const siblingIdx = idx % 2 === 0 ? idx + 1 : idx - 1;
if (siblingIdx >= 0 && siblingIdx < level.length) {
proof.push(level[siblingIdx]);
}
for (let i = 0; i < level.length; i += 2) {
const left = level[i];
const right = i + 1 < level.length ? level[i + 1] : left;
next.push(keccak256(Buffer.from(left.slice(2) + right.slice(2), 'hex')));
}
level = next;
idx = Math.floor(idx / 2);
}
return proof;
}
export interface CommitmentResult {
root: string;
startBlock: number;
endBlock: number;
chainId: number;
schemaVersion: number;
leafCount: number;
leafHashes: string[];
}
/**
* Build commitment from leaves. Returns root and metadata for submitCommit.
*/
export function buildCommitment(leaves: CommitmentLeaf[], chainId: number): CommitmentResult {
const leafHashes = leaves.map(hashLeaf);
const root = buildMerkleRoot(leafHashes);
const blockNumbers = leaves.map((l) => Number(l.blockNumber));
const startBlock = Math.min(...blockNumbers);
const endBlock = Math.max(...blockNumbers);
return {
root,
startBlock,
endBlock,
chainId,
schemaVersion: SCHEMA_VERSION,
leafCount: leaves.length,
leafHashes,
};
}

View File

@@ -0,0 +1,56 @@
/**
* In-memory store for mirror commits (MVP). In production use DB + object storage.
*/
import { buildMerkleProof } from './merkle-commitment.js';
export interface StoredCommit {
commitId: string;
chainId: number;
startBlock: number;
endBlock: number;
root: string;
uri: string;
timestamp: number;
leafHashes: string[];
leavesByTxHash: Map<string, { leafIndex: number; leafData: unknown }>;
publicChainTxHashes: string[];
createdAt: string;
}
const commits = new Map<string, StoredCommit>();
export function saveCommit(c: StoredCommit): void {
commits.set(c.commitId, c);
}
export function getCommit(commitId: string): StoredCommit | undefined {
return commits.get(commitId);
}
export function getProof(chainId: number, txHash: string): {
commitId: string;
leafData: unknown;
leafIndex: number;
leafHash: string;
proof: string[];
root: string;
publicChainTxHashes: string[];
} | null {
const txHashLower = txHash.toLowerCase();
for (const c of commits.values()) {
if (c.chainId !== chainId) continue;
const entry = c.leavesByTxHash.get(txHashLower);
if (!entry) continue;
const proof = buildMerkleProof(c.leafHashes, entry.leafIndex);
return {
commitId: c.commitId,
leafData: entry.leafData,
leafIndex: entry.leafIndex,
leafHash: c.leafHashes[entry.leafIndex],
proof,
root: c.root,
publicChainTxHashes: c.publicChainTxHashes,
};
}
return null;
}

View File

@@ -0,0 +1,47 @@
/**
* Centralized nonce allocation per (chain, wallet, lane).
* Tracks pending txs; reclaims nonce on drop/timeout.
*/
function key(chainId: number, wallet: string, lane: string): string {
return `${chainId}:${wallet.toLowerCase()}:${lane}`;
}
const pendingByKey = new Map<string, Set<string>>();
const nextNonceByKey = new Map<string, number>();
export class NonceService {
getNextNonce(chainId: number, wallet: string, lane: string): number {
const k = key(chainId, wallet, lane);
const next = nextNonceByKey.get(k) ?? 0;
nextNonceByKey.set(k, next + 1);
return next;
}
trackPending(chainId: number, wallet: string, lane: string, txHash: string): void {
const k = key(chainId, wallet, lane);
let set = pendingByKey.get(k);
if (!set) {
set = new Set();
pendingByKey.set(k, set);
}
set.add(txHash.toLowerCase());
}
releasePending(chainId: number, wallet: string, lane: string, txHash: string): void {
const k = key(chainId, wallet, lane);
const set = pendingByKey.get(k);
if (set) set.delete(txHash.toLowerCase());
}
setNextNonce(chainId: number, wallet: string, lane: string, nonce: number): void {
nextNonceByKey.set(key(chainId, wallet, lane), nonce);
}
pendingCount(chainId: number, wallet: string, lane: string): number {
const set = pendingByKey.get(key(chainId, wallet, lane));
return set?.size ?? 0;
}
}
export const nonceService = new NonceService();

View File

@@ -0,0 +1,82 @@
/**
* Transaction Router + Policy Engine (TRPE).
* Validates intents, selects execution path, enforces limits.
*/
import type { IntentRequest, PlannedStep } from '../intent/types.js';
import { getSupportedChainIds } from '../chain-adapters/config.js';
const MAX_AMOUNT = 1e30;
const DEFAULT_MAX_SLIPPAGE_BPS = 500;
const DEFAULT_TTL_MS = 300_000;
export interface ValidationResult {
valid: boolean;
error?: string;
planned_steps: PlannedStep[];
}
export function validateAndPlan(request: IntentRequest): ValidationResult {
const chainFrom = request.chain_from;
const chainTo = request.chain_to;
const supported = getSupportedChainIds();
if (!supported.includes(chainFrom) || !supported.includes(chainTo)) {
return { valid: false, error: 'Unsupported chain', planned_steps: [] };
}
const amount = parseFloat(request.amount);
if (isNaN(amount) || amount <= 0 || amount > MAX_AMOUNT) {
return { valid: false, error: 'Invalid amount', planned_steps: [] };
}
const maxSlippageBps = request.max_slippage_bps ?? DEFAULT_MAX_SLIPPAGE_BPS;
if (maxSlippageBps < 0 || maxSlippageBps > 10000) {
return { valid: false, error: 'Invalid max_slippage_bps', planned_steps: [] };
}
const steps: PlannedStep[] = [];
const TEZOS_CHAIN_ID = 1729;
const chainLabelToId: Record<string, number> = {
CHAIN138: 138,
ALL_MAINNET: 651940,
HUB_EVM: 1,
TEZOS: TEZOS_CHAIN_ID,
};
if (request.route_plan?.hops?.length && chainTo === TEZOS_CHAIN_ID) {
for (let i = 0; i < request.route_plan.hops.length; i++) {
const h = request.route_plan.hops[i];
const chainId = h.chain_id ?? chainLabelToId[h.chain] ?? (h.chain === 'CHAIN138' ? 138 : h.chain === 'ALL_MAINNET' ? 651940 : h.chain === 'TEZOS' ? TEZOS_CHAIN_ID : 1);
const stepType = h.action === 'SWAP' ? 'swap' : h.action === 'BRIDGE' ? 'bridge' : h.action === 'TRANSFER' ? 'transfer' : 'message_send';
steps.push({
step_index: i,
step_type: stepType as 'swap' | 'bridge' | 'transfer' | 'message_send' | 'message_receive',
chain_id: chainId,
});
}
return { valid: true, planned_steps: steps };
}
if (chainFrom === chainTo) {
if (request.asset_in === request.asset_out) {
steps.push({ step_index: 0, step_type: 'transfer', chain_id: chainFrom });
} else {
steps.push({ step_index: 0, step_type: 'swap', chain_id: chainFrom });
}
} else {
steps.push({
step_index: 0,
step_type: 'message_send',
chain_id: chainFrom,
postconditions: ['message_sent'],
});
steps.push({
step_index: 1,
step_type: 'message_receive',
chain_id: chainTo,
preconditions: ['message_sent'],
});
}
return { valid: true, planned_steps: steps };
}