chore: sync submodule state (parent ref update)
Made-with: Cursor
This commit is contained in:
87
orchestration/bridge/bridge-quote-routes.ts
Normal file
87
orchestration/bridge/bridge-quote-routes.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Bridge quote API routes (swap+bridge+swap).
|
||||
* Mount at POST /api/bridge/quote and POST /api/bridge/quote/swap-bridge-swap.
|
||||
* Requires RPC_URL, BRIDGE_REGISTRY_ADDRESS (and registry ABI); optional: ENHANCED_SWAP_ROUTER_ADDRESS, DESTINATION_RPC_URL, DESTINATION_SWAP_ROUTER_ADDRESS.
|
||||
* Sends CORS and Content-Type: application/json so the DApp (dapp.d-bis.org) can call cross-origin without CORB.
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { createQuoteServiceFromEnv } from './quote-service';
|
||||
import { DestinationType } from './workflow-engine';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const DAPP_ORIGINS = ['https://dapp.d-bis.org', 'http://192.168.11.58', 'http://localhost:5173'];
|
||||
function setCorsAndJson(res: Response, req: Request): void {
|
||||
const origin = req.get('origin');
|
||||
if (origin && (DAPP_ORIGINS.includes(origin) || process.env.NODE_ENV !== 'production')) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
} else if (process.env.NODE_ENV !== 'production') {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
function getQuoteService(): ReturnType<typeof createQuoteServiceFromEnv> | null {
|
||||
try {
|
||||
return createQuoteServiceFromEnv();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/bridge/quote
|
||||
* Body: { token, amount, destinationChainId, destinationType?, destinationAddress }
|
||||
* Or swap+bridge+swap: { sourceToken, destinationToken, sourceChainId, destinationChainId, amount, destinationAddress }
|
||||
*/
|
||||
router.options('/quote', (req: Request, res: Response) => {
|
||||
const origin = req.get('origin');
|
||||
if (origin && (DAPP_ORIGINS.includes(origin) || process.env.NODE_ENV !== 'production')) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||
} else if (process.env.NODE_ENV !== 'production') {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
res.setHeader('Access-Control-Max-Age', '86400');
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
router.post('/quote', async (req: Request, res: Response) => {
|
||||
setCorsAndJson(res, req);
|
||||
try {
|
||||
const svc = getQuoteService();
|
||||
if (!svc) {
|
||||
return res.status(503).json({ error: 'Quote service not configured (set RPC_URL, BRIDGE_REGISTRY_ADDRESS)' });
|
||||
}
|
||||
const body = req.body as Record<string, unknown>;
|
||||
const sourceToken = body.sourceToken as string | undefined;
|
||||
const destinationToken = body.destinationToken as string | undefined;
|
||||
const sourceChainId = body.sourceChainId as number | undefined;
|
||||
const destinationChainId = (body.destinationChainId ?? body.destinationChainId) as number;
|
||||
const amount = (body.amount ?? body.amountIn) as string;
|
||||
const token = (body.token ?? sourceToken) as string;
|
||||
const destinationAddress = (body.destinationAddress ?? body.recipient) as string;
|
||||
|
||||
if (!amount || !destinationChainId) {
|
||||
return res.status(400).json({ error: 'amount and destinationChainId are required' });
|
||||
}
|
||||
|
||||
const request = {
|
||||
token: token || '0x0000000000000000000000000000000000000000',
|
||||
amount: String(amount),
|
||||
destinationChainId: Number(destinationChainId),
|
||||
destinationType: (body.destinationType as DestinationType) ?? DestinationType.EVM,
|
||||
destinationAddress: destinationAddress || '0x0000000000000000000000000000000000000000',
|
||||
};
|
||||
|
||||
const quote = await svc.getQuote(request);
|
||||
res.json(quote);
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -24,22 +24,41 @@ export interface QuoteResponse {
|
||||
estimatedTime: number;
|
||||
slippage: string;
|
||||
riskLevel: number;
|
||||
/** Best swap quote (e.g. Dodoex) on source chain when EnhancedSwapRouter is configured */
|
||||
sourceSwapQuote?: string;
|
||||
/** Best swap quote on destination chain when swap router is configured */
|
||||
destinationSwapQuote?: string;
|
||||
}
|
||||
|
||||
const ENHANCED_SWAP_ROUTER_ABI = [
|
||||
'function getQuotes(address stablecoinToken, uint256 amountIn) view returns (uint8[] providers, uint256[] amounts)',
|
||||
];
|
||||
|
||||
export class QuoteService {
|
||||
private provider: ethers.Provider;
|
||||
private registry: ethers.Contract;
|
||||
private thirdwebClientId?: string;
|
||||
private enhancedSwapRouterAddress?: string;
|
||||
private destinationProvider?: ethers.Provider;
|
||||
private destinationSwapRouterAddress?: string;
|
||||
|
||||
constructor(
|
||||
rpcUrl: string,
|
||||
registryAddress: string,
|
||||
registryAbi: any[],
|
||||
thirdwebClientId?: string
|
||||
thirdwebClientId?: string,
|
||||
enhancedSwapRouterAddress?: string,
|
||||
destinationRpcUrl?: string,
|
||||
destinationSwapRouterAddress?: string
|
||||
) {
|
||||
this.provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
this.registry = new ethers.Contract(registryAddress, registryAbi, this.provider);
|
||||
this.thirdwebClientId = thirdwebClientId;
|
||||
this.enhancedSwapRouterAddress = enhancedSwapRouterAddress;
|
||||
if (destinationRpcUrl) {
|
||||
this.destinationProvider = new ethers.JsonRpcProvider(destinationRpcUrl);
|
||||
}
|
||||
this.destinationSwapRouterAddress = destinationSwapRouterAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,6 +108,42 @@ export class QuoteService {
|
||||
riskLevel = tokenConfig.riskLevel;
|
||||
}
|
||||
|
||||
// Optional: Dodoex-inclusive swap quotes when EnhancedSwapRouter is configured
|
||||
let sourceSwapQuote: string | undefined;
|
||||
let destinationSwapQuote: string | undefined;
|
||||
if (this.enhancedSwapRouterAddress && request.token !== ethers.ZeroAddress) {
|
||||
try {
|
||||
const router = new ethers.Contract(
|
||||
this.enhancedSwapRouterAddress,
|
||||
ENHANCED_SWAP_ROUTER_ABI,
|
||||
this.provider
|
||||
);
|
||||
const [providers, amounts] = await router.getQuotes(request.token, request.amount);
|
||||
if (amounts && amounts.length > 0) {
|
||||
const best = amounts.reduce((a: bigint, b: bigint) => (a > b ? a : b), 0n);
|
||||
sourceSwapQuote = best.toString();
|
||||
}
|
||||
} catch {
|
||||
// Router not deployed or getQuotes failed; leave undefined
|
||||
}
|
||||
}
|
||||
if (this.destinationProvider && this.destinationSwapRouterAddress && request.token !== ethers.ZeroAddress) {
|
||||
try {
|
||||
const router = new ethers.Contract(
|
||||
this.destinationSwapRouterAddress,
|
||||
ENHANCED_SWAP_ROUTER_ABI,
|
||||
this.destinationProvider
|
||||
);
|
||||
const [providers, amounts] = await router.getQuotes(request.token, request.amount);
|
||||
if (amounts && amounts.length > 0) {
|
||||
const best = amounts.reduce((a: bigint, b: bigint) => (a > b ? a : b), 0n);
|
||||
destinationSwapQuote = best.toString();
|
||||
}
|
||||
} catch {
|
||||
// Destination router not available
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
transferId: this.generateTransferId(),
|
||||
routes,
|
||||
@@ -97,7 +152,9 @@ export class QuoteService {
|
||||
minReceived: minReceived.toString(),
|
||||
estimatedTime,
|
||||
slippage,
|
||||
riskLevel
|
||||
riskLevel,
|
||||
sourceSwapQuote,
|
||||
destinationSwapQuote
|
||||
};
|
||||
}
|
||||
|
||||
@@ -243,13 +300,12 @@ export class QuoteService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Fabric route
|
||||
* Get Fabric route. Set FABRIC_CHAIN_ID in env when Fabric is live; default 999 until then.
|
||||
*/
|
||||
private async getFabricRoute(token: string, amount: string): Promise<RouteInfo | null> {
|
||||
// Fabric typically uses a different chainId or identifier
|
||||
// For now, use a placeholder
|
||||
const fabricChainId = process.env.FABRIC_CHAIN_ID ? parseInt(process.env.FABRIC_CHAIN_ID, 10) : 999;
|
||||
return {
|
||||
chainId: 999, // Placeholder for Fabric
|
||||
chainId: fabricChainId,
|
||||
chainName: 'Hyperledger Fabric',
|
||||
provider: 'cacti-fabric',
|
||||
estimatedTime: 120, // Fabric settlement time
|
||||
@@ -315,3 +371,26 @@ export class QuoteService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create QuoteService from environment variables (for bridge API / orchestration).
|
||||
* Set RPC_URL, BRIDGE_REGISTRY_ADDRESS; optional: ENHANCED_SWAP_ROUTER_ADDRESS,
|
||||
* DESTINATION_RPC_URL, DESTINATION_SWAP_ROUTER_ADDRESS, THIRDWEB_CLIENT_ID.
|
||||
*/
|
||||
export function createQuoteServiceFromEnv(): QuoteService {
|
||||
const rpcUrl = process.env.RPC_URL || process.env.RPC_URL_138 || '';
|
||||
const registryAddress = process.env.BRIDGE_REGISTRY_ADDRESS || '';
|
||||
const registryAbi: any[] = []; // Bridge registry ABI - load from contract if needed
|
||||
if (!rpcUrl || !registryAddress) {
|
||||
throw new Error('RPC_URL (or RPC_URL_138) and BRIDGE_REGISTRY_ADDRESS are required');
|
||||
}
|
||||
return new QuoteService(
|
||||
rpcUrl,
|
||||
registryAddress,
|
||||
registryAbi,
|
||||
process.env.THIRDWEB_CLIENT_ID,
|
||||
process.env.ENHANCED_SWAP_ROUTER_ADDRESS,
|
||||
process.env.DESTINATION_RPC_URL,
|
||||
process.env.DESTINATION_SWAP_ROUTER_ADDRESS
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"dotenv": "^16.3.1",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.22.3",
|
||||
@@ -52,6 +53,7 @@
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
/**
|
||||
* Admin authentication middleware
|
||||
* Simple token-based auth for now (can be enhanced with JWT later)
|
||||
* Supports: (1) JWT via Authorization Bearer (when ADMIN_JWT_SECRET or JWT_SECRET set),
|
||||
* (2) x-admin-token session (backward compatible)
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const JWT_SECRET = process.env.ADMIN_JWT_SECRET || process.env.JWT_SECRET;
|
||||
|
||||
// Simple in-memory session store (replace with Redis in production)
|
||||
const sessions = new Map<string, { username: string; expires: number }>();
|
||||
@@ -13,10 +17,22 @@ export interface AuthRequest extends Request {
|
||||
}
|
||||
|
||||
export function requireAdmin(req: AuthRequest, res: Response, next: NextFunction): void {
|
||||
const bearer = req.headers.authorization;
|
||||
if (bearer?.startsWith('Bearer ') && JWT_SECRET) {
|
||||
const token = bearer.slice(7);
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as { sub?: string; username?: string };
|
||||
req.adminUser = decoded.sub ?? decoded.username ?? 'unknown';
|
||||
return next();
|
||||
} catch {
|
||||
res.status(401).json({ error: 'Invalid or expired JWT' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const token = req.headers['x-admin-token'] as string;
|
||||
|
||||
if (!token) {
|
||||
res.status(401).json({ error: 'Admin token required' });
|
||||
res.status(401).json({ error: 'Admin token or Bearer JWT required' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,6 +48,13 @@ export function requireAdmin(req: AuthRequest, res: Response, next: NextFunction
|
||||
}
|
||||
|
||||
export function createSession(username: string): string {
|
||||
if (JWT_SECRET) {
|
||||
return jwt.sign(
|
||||
{ sub: username, iat: Math.floor(Date.now() / 1000) },
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
}
|
||||
const token = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
sessions.set(token, {
|
||||
username,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { DatabaseManager } from './database';
|
||||
import { Environment, DeploymentRequest, Deployment, Alert, Cost } from './types';
|
||||
import { requireAdmin, createSession, getClientIp, AuthRequest } from './middleware/auth';
|
||||
import { MonitoringService } from './services/monitoring';
|
||||
import { appendAudit } from './services/central-audit';
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
@@ -420,6 +421,7 @@ app.put('/api/admin/services/:name', requireAdmin, (req: AuthRequest, res: Respo
|
||||
|
||||
db.setServiceConfig(name, enabled !== false, config || null, adminUser);
|
||||
db.logAdminAction(adminUser, 'update_service', 'service', name, JSON.stringify({ enabled, config }), ipAddress);
|
||||
appendAudit({ employeeId: adminUser, action: 'update_service', permission: 'admin:action', resourceType: 'service', resourceId: name, ipAddress, userAgent: req.headers['user-agent'] as string }).catch(() => {});
|
||||
|
||||
// Broadcast real-time update
|
||||
broadcastAdminUpdate('service-updated', { service_name: name, enabled, updated_by: adminUser });
|
||||
@@ -465,6 +467,7 @@ app.put('/api/admin/providers/:name', requireAdmin, (req: AuthRequest, res: Resp
|
||||
|
||||
db.setProviderConfig(name, enabled !== false, config || null, adminUser);
|
||||
db.logAdminAction(adminUser, 'update_provider', 'provider', name, JSON.stringify({ enabled, config }), ipAddress);
|
||||
appendAudit({ employeeId: adminUser, action: 'update_provider', permission: 'admin:action', resourceType: 'provider', resourceId: name, ipAddress, userAgent: req.headers['user-agent'] as string }).catch(() => {});
|
||||
|
||||
// Broadcast real-time update
|
||||
broadcastAdminUpdate('provider-updated', { provider_name: name, enabled, updated_by: adminUser });
|
||||
@@ -503,6 +506,7 @@ app.put('/api/admin/environments/:name/toggle', requireAdmin, (req: AuthRequest,
|
||||
// Update environment in YAML file
|
||||
config.updateEnvironmentEnabled(name, enabled !== false);
|
||||
db.logAdminAction(adminUser, 'toggle_environment', 'environment', name, JSON.stringify({ enabled }), ipAddress);
|
||||
appendAudit({ employeeId: adminUser, action: 'toggle_environment', permission: 'admin:action', resourceType: 'environment', resourceId: name, metadata: { enabled }, ipAddress, userAgent: req.headers['user-agent'] as string }).catch(() => {});
|
||||
|
||||
// Broadcast real-time update
|
||||
broadcastAdminUpdate('environment-updated', { environment_name: name, enabled, updated_by: adminUser });
|
||||
@@ -675,7 +679,7 @@ const io = new SocketIOServer(server, {
|
||||
});
|
||||
|
||||
// Socket.IO connection handling
|
||||
io.on('connection', (socket) => {
|
||||
io.on('connection', (socket: import('socket.io').Socket) => {
|
||||
console.log('Client connected:', socket.id);
|
||||
|
||||
// Join admin room for real-time updates
|
||||
|
||||
88
orchestration/portal/src/services/central-audit.ts
Normal file
88
orchestration/portal/src/services/central-audit.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Central audit client for orchestration portal
|
||||
* Sends admin action audit entries to dbis_core Admin Central API when configured.
|
||||
* Set DBIS_CENTRAL_URL and ADMIN_CENTRAL_API_KEY to enable.
|
||||
*/
|
||||
|
||||
const DBIS_CENTRAL_URL = process.env.DBIS_CENTRAL_URL?.replace(/\/$/, '');
|
||||
const ADMIN_CENTRAL_API_KEY = process.env.ADMIN_CENTRAL_API_KEY;
|
||||
const SERVICE_NAME = 'orchestration_portal';
|
||||
|
||||
export interface AuditPayload {
|
||||
employeeId: string;
|
||||
action: string;
|
||||
permission: string;
|
||||
resourceType: string;
|
||||
resourceId?: string;
|
||||
outcome?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
}
|
||||
|
||||
function isConfigured(): boolean {
|
||||
return Boolean(DBIS_CENTRAL_URL && ADMIN_CENTRAL_API_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append an audit entry to the central audit log (dbis_core).
|
||||
* No-op if DBIS_CENTRAL_URL or ADMIN_CENTRAL_API_KEY is not set.
|
||||
*/
|
||||
export async function appendAudit(payload: AuditPayload): 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: 'smom-dbis-138',
|
||||
service: SERVICE_NAME,
|
||||
outcome: payload.outcome ?? 'success',
|
||||
metadata: payload.metadata,
|
||||
ipAddress: payload.ipAddress,
|
||||
userAgent: payload.userAgent,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.warn(`[central-audit] POST audit failed: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[central-audit] append failed:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if subject has permission via central API.
|
||||
* Returns false if not configured or on error.
|
||||
*/
|
||||
export async function checkPermission(
|
||||
subjectId: string,
|
||||
permission: string
|
||||
): Promise<boolean> {
|
||||
if (!isConfigured()) return false;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${DBIS_CENTRAL_URL}/api/admin/central/permission-check`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Admin-Central-Key': ADMIN_CENTRAL_API_KEY!,
|
||||
},
|
||||
body: JSON.stringify({ subjectId, permission }),
|
||||
});
|
||||
if (!res.ok) return false;
|
||||
const data = (await res.json()) as { success?: boolean; allowed?: boolean };
|
||||
return Boolean(data?.allowed);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user