Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m3s
CI/CD Pipeline / Security Scanning (push) Successful in 2m58s
CI/CD Pipeline / Lint and Format (push) Failing after 46s
CI/CD Pipeline / Terraform Validation (push) Failing after 26s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 29s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 41s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 22s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 24s
Validation / validate-genesis (push) Successful in 32s
Validation / validate-terraform (push) Failing after 28s
Validation / validate-kubernetes (push) Failing after 13s
Validation / validate-smart-contracts (push) Failing after 12s
Validation / validate-security (push) Failing after 1m31s
Validation / validate-documentation (push) Failing after 23s
Verify Deployment / Verify Deployment (push) Failing after 1m4s
Add BTC L1 settle/reconcile/ledger APIs, bitcoind intake, cBTC PMM hot-LP scripts, and custody credential smoke tests (secrets stay gitignored). Enables full-prod local green health and server pull-deploy for secure.omdnl.org /btc/*. Co-authored-by: Cursor <cursoragent@cursor.com>
599 lines
19 KiB
TypeScript
599 lines
19 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
|
import { Contract, JsonRpcProvider, type InterfaceAbi } from 'ethers';
|
|
import { omnlRateLimiter } from '../middleware/rate-limit';
|
|
import { omnlRequireApiKeyInProduction } from '../middleware/omnl-guards';
|
|
import { omnlAuditMiddleware } from '../middleware/omnl-audit-middleware';
|
|
import {
|
|
fetchOmnlCompliance,
|
|
fetchOmnlComplianceAggregated,
|
|
fetchLatestAttestation,
|
|
fetchBreakerStatus,
|
|
loadCrossChainLines,
|
|
loadCrossChainConfigPath,
|
|
} from '../../services/omnl-compliance';
|
|
import { omnlComplianceCore138, omnlReserveStore138 } from '../../services/omnl-chain138-addresses';
|
|
import { computeOmnlReconcileAnchor } from '../../services/omnl-reconcile-anchor';
|
|
import { getOmnlIntegrationStatus } from '../../services/omnl-integration-status';
|
|
import { getOmnlApiCatalog } from '../../services/omnl-api-catalog';
|
|
import { loadGuosmmNotices, getGuosmmNotice } from '../../services/omnl-gazette-notices';
|
|
import omnlOpenApi from '../../resources/omnl-openapi.json';
|
|
import {
|
|
executeErc20Transfer,
|
|
executeM2TokenMint,
|
|
chainRailConfigured,
|
|
} from '../../services/omnl-settlement-chain';
|
|
import {
|
|
convertFiatLpToLiquidity,
|
|
readFiatLpPositions,
|
|
} from '../../services/omnl-fiat-lp-liquidity';
|
|
import { loadFiatLpParityConfig } from '../../config/fiat-lp-liquidity';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
function resolveMintTokenAddress(lineId: string, symbol?: string, tokenAddress?: string): string | undefined {
|
|
if (tokenAddress?.startsWith('0x')) return tokenAddress;
|
|
const registryPath =
|
|
process.env.OMNL_M2_TOKEN_REGISTRY ||
|
|
path.resolve(__dirname, '../../../../config/omnl-m2-token-registry.v1.json');
|
|
try {
|
|
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')) as {
|
|
tokens: Array<{ omnlLine: string; symbol: string; address: string }>;
|
|
};
|
|
const id = lineId.trim().toUpperCase();
|
|
const byLine = registry.tokens.find((t) => t.omnlLine.toUpperCase() === id);
|
|
if (byLine?.address.startsWith('0x') && byLine.address !== '0x0000000000000000000000000000000000000000') {
|
|
return byLine.address;
|
|
}
|
|
if (symbol) {
|
|
const bySym = registry.tokens.find((t) => t.symbol.toLowerCase() === symbol.toLowerCase());
|
|
if (bySym?.address.startsWith('0x') && bySym.address !== '0x0000000000000000000000000000000000000000') {
|
|
return bySym.address;
|
|
}
|
|
}
|
|
} catch {
|
|
/* registry optional */
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
const router = Router();
|
|
router.use(omnlRateLimiter);
|
|
router.use(omnlAuditMiddleware);
|
|
|
|
/**
|
|
* Public discovery + gazette (registered before production API key gate).
|
|
*/
|
|
router.get('/omnl/openapi.json', (_req: Request, res: Response) => {
|
|
res.type('application/json').json(omnlOpenApi as Record<string, unknown>);
|
|
});
|
|
|
|
router.get('/omnl/catalog', (_req: Request, res: Response) => {
|
|
res.json(getOmnlApiCatalog());
|
|
});
|
|
|
|
router.get('/omnl/integration-status', (_req: Request, res: Response) => {
|
|
res.json({
|
|
generatedAt: new Date().toISOString(),
|
|
...getOmnlIntegrationStatus(),
|
|
});
|
|
});
|
|
|
|
router.get('/omnl/gazette/notices', (_req: Request, res: Response) => {
|
|
const reg = loadGuosmmNotices();
|
|
res.json({
|
|
generatedAt: new Date().toISOString(),
|
|
gazetteBase: reg.gazetteBase,
|
|
count: reg.notices.length,
|
|
notices: reg.notices,
|
|
});
|
|
});
|
|
|
|
router.get('/omnl/gazette/notices/:documentId', (req: Request, res: Response) => {
|
|
const documentId = String(req.params.documentId ?? '');
|
|
const notice = getGuosmmNotice(documentId);
|
|
if (!notice) {
|
|
res.status(404).json({ error: 'Notice not found' });
|
|
return;
|
|
}
|
|
res.json(notice);
|
|
});
|
|
|
|
router.use(omnlRequireApiKeyInProduction);
|
|
|
|
const REGISTRY_ABI: InterfaceAbi = [
|
|
'function allLineIds() view returns (bytes32[])',
|
|
'function getLine(bytes32 lineId) view returns (tuple(address tokenM0,address tokenM1,uint8 decimals,uint16 iso4217Numeric,bool isXAU,bool active))',
|
|
];
|
|
|
|
const COORDINATOR_ABI: InterfaceAbi = [
|
|
'function mirrorChainSelector() view returns (uint64)',
|
|
'function mirrorReceiver() view returns (address)',
|
|
'function feeToken() view returns (address)',
|
|
];
|
|
|
|
/**
|
|
* GET /omnl/reconcile-anchor — same SHA-256 as omnl-reconcile-report.mjs (IPSAS + matrix files).
|
|
*/
|
|
router.get('/omnl/reconcile-anchor', (_req: Request, res: Response) => {
|
|
const out = computeOmnlReconcileAnchor();
|
|
if (!out.ok) {
|
|
res.status(503).json(out);
|
|
return;
|
|
}
|
|
res.json(out);
|
|
});
|
|
|
|
/**
|
|
* GET /omnl/cross-chain-lines — logical lines from hybx-omnl-cross-chain-lines.json.
|
|
*/
|
|
router.get('/omnl/cross-chain-lines', (_req: Request, res: Response) => {
|
|
try {
|
|
res.json({
|
|
configPath: loadCrossChainConfigPath(),
|
|
lines: loadCrossChainLines(),
|
|
});
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
res.status(500).json({ error: msg });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /omnl/mirror-coordinator?chainId=138 — on-chain mirror destination (CCIP).
|
|
*/
|
|
/**
|
|
* GET /omnl/zk-verifier — env-configured verifier address (placeholder ok).
|
|
*/
|
|
router.get('/omnl/zk-verifier', (_req: Request, res: Response) => {
|
|
const a = process.env.OMNL_ZK_VERIFIER?.trim() || '';
|
|
res.json({
|
|
configured: Boolean(a),
|
|
address: a || null,
|
|
note: 'See docs/hybx-omnl/ZK_INTEGRATION.md for wiring a real verifier.',
|
|
});
|
|
});
|
|
|
|
router.get('/omnl/mirror-coordinator', async (req: Request, res: Response) => {
|
|
try {
|
|
const chainId = parseInt(String(req.query.chainId || '138'), 10);
|
|
const coord =
|
|
process.env[`OMNL_MIRROR_COORDINATOR_${chainId}`]?.trim() ||
|
|
process.env.OMNL_MIRROR_COORDINATOR?.trim();
|
|
if (!coord) {
|
|
res.status(503).json({
|
|
error: 'OMNL_MIRROR_COORDINATOR or OMNL_MIRROR_COORDINATOR_<chainId> not set',
|
|
chainId,
|
|
});
|
|
return;
|
|
}
|
|
const rpc = rpcUrl(chainId);
|
|
if (!rpc) {
|
|
res.status(503).json({ error: 'RPC not configured for chain', chainId });
|
|
return;
|
|
}
|
|
const c = new Contract(coord, COORDINATOR_ABI, new JsonRpcProvider(rpc, chainId));
|
|
const [mirrorChainSelector, mirrorReceiver, feeToken] = await Promise.all([
|
|
c.mirrorChainSelector(),
|
|
c.mirrorReceiver(),
|
|
c.feeToken(),
|
|
]);
|
|
res.json({
|
|
chainId,
|
|
coordinator: coord,
|
|
mirrorChainSelector: String(mirrorChainSelector),
|
|
mirrorReceiver,
|
|
feeToken,
|
|
});
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
res.status(500).json({ error: msg });
|
|
}
|
|
});
|
|
|
|
function addrForChain(chainId: number): string | undefined {
|
|
if (chainId === 138) {
|
|
return omnlComplianceCore138();
|
|
}
|
|
if (chainId === 651940) {
|
|
return process.env.OMNL_COMPLIANCE_CORE_651940;
|
|
}
|
|
return process.env[`OMNL_COMPLIANCE_CORE_${chainId}`];
|
|
}
|
|
|
|
function rpcUrl(chainId: number): string | undefined {
|
|
if (chainId === 138) return process.env.RPC_URL_138 || process.env.RPC_URL;
|
|
if (chainId === 651940) return process.env.CHAIN_651940_RPC_URL || 'https://mainnet-rpc.alltra.global';
|
|
return process.env[`CHAIN_${chainId}_RPC_URL`];
|
|
}
|
|
|
|
/**
|
|
* GET /omnl/compliance/:lineId?chainId=138|651940
|
|
*/
|
|
router.get('/omnl/compliance/:lineId', async (req: Request, res: Response) => {
|
|
try {
|
|
const chainId = parseInt(String(req.query.chainId || '138'), 10);
|
|
const lineId = req.params.lineId as string;
|
|
const addr = addrForChain(chainId);
|
|
if (!addr) {
|
|
res.status(503).json({
|
|
error: 'OMNL_COMPLIANCE_CORE not configured for this chain',
|
|
chainId,
|
|
});
|
|
return;
|
|
}
|
|
const snap = await fetchOmnlCompliance(chainId, lineId, addr);
|
|
res.json(snap);
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
res.status(500).json({ error: msg });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /omnl/compliance-aggregated/:lineId — summed supply across chains (see hybx-omnl-cross-chain-lines.json).
|
|
*/
|
|
router.get('/omnl/compliance-aggregated/:lineId', async (req: Request, res: Response) => {
|
|
try {
|
|
const lineId = req.params.lineId as string;
|
|
const snap = await fetchOmnlComplianceAggregated(lineId);
|
|
res.json(snap);
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
res.status(500).json({ error: msg });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /omnl/instruments?chainId=138 — InstrumentRegistry.allLineIds + getLine
|
|
*/
|
|
router.get('/omnl/instruments', async (req: Request, res: Response) => {
|
|
try {
|
|
const chainId = parseInt(String(req.query.chainId || '138'), 10);
|
|
const reg = process.env[`OMNL_INSTRUMENT_REGISTRY_${chainId}`] || process.env.OMNL_INSTRUMENT_REGISTRY_138;
|
|
const rpc = rpcUrl(chainId);
|
|
if (!reg || !rpc) {
|
|
res.status(503).json({ error: 'OMNL_INSTRUMENT_REGISTRY_* and RPC required', chainId });
|
|
return;
|
|
}
|
|
const c = new Contract(reg, REGISTRY_ABI, new JsonRpcProvider(rpc, chainId));
|
|
const ids = (await c.allLineIds()) as string[];
|
|
const lines = await Promise.all(
|
|
ids.map(async (id: string) => {
|
|
const line = await c.getLine(id);
|
|
return { lineId: id, line };
|
|
})
|
|
);
|
|
res.json({ chainId, lines, crossChainConfigLines: loadCrossChainLines().length });
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
res.status(500).json({ error: msg });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /omnl/attestations/:lineId?chainId=138 — ReserveCommitmentStore.getCommitment
|
|
*/
|
|
router.get('/omnl/attestations/:lineId', async (req: Request, res: Response) => {
|
|
try {
|
|
const chainId = parseInt(String(req.query.chainId || '138'), 10);
|
|
const lineId = req.params.lineId as string;
|
|
const out = await fetchLatestAttestation(chainId, lineId);
|
|
res.json({ chainId, lineId, ...out });
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
res.status(500).json({ error: msg });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /omnl/breaker?chainId=138&lineId=0x...
|
|
*/
|
|
router.get('/omnl/breaker', async (req: Request, res: Response) => {
|
|
try {
|
|
const chainId = parseInt(String(req.query.chainId || '138'), 10);
|
|
const lineId = String(req.query.lineId || '');
|
|
if (!lineId || lineId === 'undefined') {
|
|
res.status(400).json({ error: 'lineId query parameter required' });
|
|
return;
|
|
}
|
|
const s = await fetchBreakerStatus(chainId, lineId);
|
|
res.json({ chainId, lineId, ...s });
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
res.status(500).json({ error: msg });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /omnl/mirror-status/:lineId — compare reserve version (and R) across 138 and 651940.
|
|
*/
|
|
router.get('/omnl/mirror-status/:lineId', async (req: Request, res: Response) => {
|
|
try {
|
|
const lineId = req.params.lineId as string;
|
|
const a138 = omnlReserveStore138();
|
|
const a651940 = process.env.OMNL_RESERVE_STORE_651940;
|
|
const out: Record<string, unknown> = {
|
|
lineId,
|
|
configured138: Boolean(a138),
|
|
configured651940: Boolean(a651940),
|
|
};
|
|
if (!a138 || !a651940) {
|
|
res.status(503).json({
|
|
...out,
|
|
error: 'Set OMNL_RESERVE_STORE_138 and OMNL_RESERVE_STORE_651940 for mirror comparison',
|
|
});
|
|
return;
|
|
}
|
|
const [x138, x651] = await Promise.all([
|
|
fetchLatestAttestation(138, lineId),
|
|
fetchLatestAttestation(651940, lineId),
|
|
]);
|
|
out.attestation138 = x138;
|
|
out.attestation651940 = x651;
|
|
out.rSynced = x138.r === x651.r;
|
|
out.versionSynced = x138.version === x651.version;
|
|
out.merkleSynced = x138.merkleRoot === x651.merkleRoot;
|
|
out.inSync = out.rSynced && out.versionSynced;
|
|
res.json(out);
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
res.status(500).json({ error: msg });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /omnl/health — requires OMNL_HEALTH_LINE_ID when comparing chains (no default zero line).
|
|
*/
|
|
router.get('/omnl/health', async (req: Request, res: Response) => {
|
|
const line = process.env.OMNL_HEALTH_LINE_ID;
|
|
if (!line) {
|
|
res.status(400).json({
|
|
error: 'Set OMNL_HEALTH_LINE_ID to a registered bytes32 line id (0x-prefixed)',
|
|
});
|
|
return;
|
|
}
|
|
const a138 = omnlComplianceCore138();
|
|
const a651940 = process.env.OMNL_COMPLIANCE_CORE_651940;
|
|
const out: Record<string, unknown> = {
|
|
lineId: line,
|
|
configured138: Boolean(a138),
|
|
configured651940: Boolean(a651940),
|
|
};
|
|
try {
|
|
if (a138) {
|
|
out.compliance138 = await fetchOmnlCompliance(138, line, a138);
|
|
}
|
|
if (a651940) {
|
|
out.compliance651940 = await fetchOmnlCompliance(651940, line, a651940);
|
|
}
|
|
if (a138 && a651940 && out.compliance138 && out.compliance651940) {
|
|
const x = out.compliance138 as { r: string; reportingCompliant: boolean };
|
|
const y = out.compliance651940 as { r: string; reportingCompliant: boolean };
|
|
out.rSynced = x.r === y.r;
|
|
out.reportingSynced =
|
|
x.reportingCompliant === y.reportingCompliant && x.r === y.r;
|
|
}
|
|
res.json(out);
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
res.status(500).json({ ...out, error: msg });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /omnl/settlement/token-load — M2→M3 fiat-backed token mint (Office 24 settlement).
|
|
*/
|
|
router.post('/omnl/settlement/token-load', async (req: Request, res: Response) => {
|
|
const { lineId, amount, recipient, settlementRef, dryRun, symbol, tokenAddress } = req.body as {
|
|
lineId?: string;
|
|
amount?: string;
|
|
recipient?: string;
|
|
settlementRef?: string;
|
|
dryRun?: boolean;
|
|
symbol?: string;
|
|
tokenAddress?: string;
|
|
};
|
|
if (!lineId || !amount || !recipient) {
|
|
res.status(400).json({ error: 'lineId, amount, recipient required' });
|
|
return;
|
|
}
|
|
const execute =
|
|
!dryRun &&
|
|
(process.env.SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE === '1' ||
|
|
process.env.OMNL_ALLOW_CHAIN_MINT_EXECUTE === '1');
|
|
const loadId = settlementRef ?? `TL-${Date.now()}`;
|
|
const capabilities = {
|
|
swappable: true,
|
|
convertible: true,
|
|
transferableInternal: true,
|
|
transferableExternal: true,
|
|
};
|
|
|
|
let mintTokenAddress = resolveMintTokenAddress(lineId, symbol, tokenAddress);
|
|
|
|
if (!execute) {
|
|
res.json({
|
|
status: 'DRY_RUN',
|
|
loadId,
|
|
lineId,
|
|
amount,
|
|
recipient,
|
|
symbol: symbol ?? null,
|
|
tokenAddress: mintTokenAddress ?? null,
|
|
settlementRef: settlementRef ?? null,
|
|
moneyLayer: 'M3',
|
|
loadFromGl: '2200',
|
|
creditGl: '2300',
|
|
capabilities,
|
|
txHash: null,
|
|
chainRailConfigured: chainRailConfigured(),
|
|
message: 'M2→M3 token load validated — set SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=1 to mint on-chain',
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!mintTokenAddress?.startsWith('0x')) {
|
|
res.status(400).json({
|
|
error: 'tokenAddress required for on-chain mint',
|
|
status: 'QUEUED',
|
|
loadId,
|
|
capabilities,
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { txHash } = await executeM2TokenMint({
|
|
tokenAddress: mintTokenAddress,
|
|
recipient,
|
|
amount,
|
|
});
|
|
res.json({
|
|
status: 'SETTLED',
|
|
loadId,
|
|
lineId,
|
|
amount,
|
|
recipient,
|
|
symbol: symbol ?? null,
|
|
tokenAddress: mintTokenAddress,
|
|
settlementRef: settlementRef ?? null,
|
|
moneyLayer: 'M3',
|
|
capabilities,
|
|
txHash,
|
|
message: 'M3 token minted on Chain 138 — tradable, swappable, transferable',
|
|
});
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
res.status(502).json({
|
|
error: msg,
|
|
status: 'CHAIN_MINT_FAILED',
|
|
loadId,
|
|
lineId,
|
|
capabilities,
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /omnl/settlement/token-transfer — ERC-20 transfer to web3 wallet (internal rail).
|
|
*/
|
|
router.post('/omnl/settlement/token-transfer', async (req: Request, res: Response) => {
|
|
const { tokenAddress, amount, recipient, settlementRef, dryRun, symbol } = req.body as {
|
|
tokenAddress?: string;
|
|
amount?: string;
|
|
recipient?: string;
|
|
settlementRef?: string;
|
|
dryRun?: boolean;
|
|
symbol?: string;
|
|
};
|
|
if (!tokenAddress || !amount || !recipient) {
|
|
res.status(400).json({ error: 'tokenAddress, amount, recipient required' });
|
|
return;
|
|
}
|
|
const execute =
|
|
!dryRun &&
|
|
(process.env.SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE === '1' ||
|
|
process.env.OMNL_ALLOW_CHAIN_MINT_EXECUTE === '1');
|
|
|
|
if (!execute) {
|
|
res.json({
|
|
status: 'DRY_RUN',
|
|
transferId: settlementRef ?? `TT-${Date.now()}`,
|
|
tokenAddress,
|
|
amount,
|
|
recipient,
|
|
symbol: symbol ?? null,
|
|
txHash: null,
|
|
message: 'Transfer validated — enable SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=1 for on-chain delivery',
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { txHash } = await executeErc20Transfer({ tokenAddress, recipient, amount });
|
|
res.json({
|
|
status: 'SETTLED',
|
|
transferId: settlementRef ?? `TT-${Date.now()}`,
|
|
tokenAddress,
|
|
amount,
|
|
recipient,
|
|
symbol: symbol ?? null,
|
|
txHash,
|
|
message: 'ERC-20 transferred to recipient wallet on Chain 138',
|
|
});
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
res.status(502).json({ error: msg, status: 'CHAIN_TRANSFER_FAILED' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /omnl/settlement/fiat-lp-positions — read c* LP balances + quoted real-fund value.
|
|
*/
|
|
router.get('/omnl/settlement/fiat-lp-positions', async (req: Request, res: Response) => {
|
|
const holder = String(req.query.holder ?? '').trim();
|
|
if (!holder.startsWith('0x')) {
|
|
res.status(400).json({ error: 'holder query param (0x address) required' });
|
|
return;
|
|
}
|
|
try {
|
|
const positions = await readFiatLpPositions(holder);
|
|
const cfg = loadFiatLpParityConfig();
|
|
res.json({
|
|
chainId: cfg.chainId,
|
|
holder,
|
|
contracts: cfg.contracts,
|
|
positions,
|
|
chainRailConfigured: chainRailConfigured(),
|
|
});
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
res.status(500).json({ error: msg });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /omnl/settlement/fiat-lp-to-liquidity — swap c* LP tokens → real USDT/USDC/WETH on Chain 138.
|
|
*/
|
|
router.post('/omnl/settlement/fiat-lp-to-liquidity', async (req: Request, res: Response) => {
|
|
const body = req.body as {
|
|
holderAddress?: string;
|
|
recipientAddress?: string;
|
|
symbol?: string;
|
|
amount?: string;
|
|
convertAll?: boolean;
|
|
maxSlippageBps?: number;
|
|
dryRun?: boolean;
|
|
settlementRef?: string;
|
|
};
|
|
if (!body.holderAddress?.startsWith('0x')) {
|
|
res.status(400).json({ error: 'holderAddress (0x) required' });
|
|
return;
|
|
}
|
|
const execute =
|
|
body.dryRun === false &&
|
|
(process.env.SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE === '1' ||
|
|
process.env.OMNL_ALLOW_CHAIN_MINT_EXECUTE === '1');
|
|
const dryRun = body.dryRun !== false && !execute;
|
|
|
|
try {
|
|
const result = await convertFiatLpToLiquidity({
|
|
holderAddress: body.holderAddress,
|
|
recipientAddress: body.recipientAddress,
|
|
symbol: body.symbol,
|
|
amount: body.amount,
|
|
convertAll: Boolean(body.convertAll),
|
|
maxSlippageBps: body.maxSlippageBps,
|
|
dryRun,
|
|
settlementRef: body.settlementRef ?? `FLP-${Date.now()}`,
|
|
});
|
|
const failed = result.swaps.some((s) => s.status === 'FAILED');
|
|
res.status(failed && !dryRun ? 422 : 200).json(result);
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
res.status(502).json({ error: msg });
|
|
}
|
|
});
|
|
|
|
export default router;
|