Add HO nostro cash-in-bank and reserve coverage to token-aggregation.
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m59s
CI/CD Pipeline / Security Scanning (push) Successful in 2m54s
CI/CD Pipeline / Lint and Format (push) Failing after 45s
CI/CD Pipeline / Terraform Validation (push) Failing after 28s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 28s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 31s
Validation / validate-genesis (push) Successful in 34s
Validation / validate-terraform (push) Failing after 26s
Validation / validate-kubernetes (push) Failing after 12s
Validation / validate-smart-contracts (push) Failing after 11s
Validation / validate-security (push) Failing after 1m25s
Validation / validate-documentation (push) Failing after 19s
Verify Deployment / Verify Deployment (push) Failing after 1m30s
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m59s
CI/CD Pipeline / Security Scanning (push) Successful in 2m54s
CI/CD Pipeline / Lint and Format (push) Failing after 45s
CI/CD Pipeline / Terraform Validation (push) Failing after 28s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 28s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 31s
Validation / validate-genesis (push) Successful in 34s
Validation / validate-terraform (push) Failing after 26s
Validation / validate-kubernetes (push) Failing after 12s
Validation / validate-smart-contracts (push) Failing after 11s
Validation / validate-security (push) Failing after 1m25s
Validation / validate-documentation (push) Failing after 19s
Verify Deployment / Verify Deployment (push) Failing after 1m30s
Expose Fineract GL 13010 nostro cash, aggregate c* reserve coverage, PMM cash liquidity, and a public ho-liquidity-snapshot API for the explorer three-line stack and 30-minute audit collector. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { cacheMiddleware } from '../middleware/cache';
|
||||
import { buildHoLiquiditySnapshot } from '../../services/omnl-ho-liquidity-snapshot';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /omnl/ho-liquidity-snapshot — public read-only HO nostro + reserve coverage (cached).
|
||||
* Refreshed on 30-minute operator timer; ?refresh=1 forces Fineract replay when configured.
|
||||
*/
|
||||
router.get('/omnl/ho-liquidity-snapshot', cacheMiddleware(30 * 60 * 1000), async (req: Request, res: Response) => {
|
||||
try {
|
||||
const forceRefresh = req.query.refresh === '1';
|
||||
const officeId = req.query.officeId != null ? Number(req.query.officeId) : undefined;
|
||||
const snapshot = await buildHoLiquiditySnapshot({
|
||||
officeId: Number.isFinite(officeId) ? officeId : undefined,
|
||||
forceRefresh,
|
||||
lineId: String(req.query.lineId || '') || undefined,
|
||||
});
|
||||
|
||||
const hasCritical = snapshot.alerts.some((a) => a.severity === 'critical');
|
||||
const status = snapshot.fineractConfigured && !hasCritical ? 200 : hasCritical ? 503 : 409;
|
||||
|
||||
res.setHeader('Cache-Control', 'public, max-age=300, stale-while-revalidate=900');
|
||||
res.setHeader('X-OMNL-HO-Nostro-Audit-At', snapshot.generatedAt);
|
||||
res.status(status).json(snapshot);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
res.status(500).json({ error: msg });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -5,6 +5,8 @@ import { omnlAuditMiddleware } from '../middleware/omnl-audit-middleware';
|
||||
import { omnlComplianceGateMiddleware } from '../middleware/omnl-compliance-gate';
|
||||
import { omnlIdempotencyMiddleware } from '../middleware/omnl-idempotency';
|
||||
import { runTripleStateReconcile } from '../../services/omnl-triple-reconcile';
|
||||
import { getCachedCashInBankAuditSnapshot } from '../../services/omnl-cash-in-bank-audit';
|
||||
import { getCachedHoReserveCoverageSnapshot } from '../../services/chain138-ho-reserve-coverage';
|
||||
import {
|
||||
buildIfrs7Disclosure,
|
||||
buildIfrs9Disclosure,
|
||||
@@ -49,6 +51,45 @@ router.get('/omnl/reconcile/triple-state', omnlSensitiveRouteGuard, async (req:
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/omnl/cash-in-bank', omnlSensitiveRouteGuard, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const officeId = req.query.officeId != null ? Number(req.query.officeId) : undefined;
|
||||
const totalSupplyUsdRaw = req.query.totalSupplyUsd != null ? Number(req.query.totalSupplyUsd) : undefined;
|
||||
const forceRefresh = String(req.query.refresh || '') === '1';
|
||||
const totalSupplyUsd =
|
||||
totalSupplyUsdRaw != null && Number.isFinite(totalSupplyUsdRaw) && totalSupplyUsdRaw > 0
|
||||
? totalSupplyUsdRaw
|
||||
: undefined;
|
||||
const snapshot = await getCachedCashInBankAuditSnapshot({
|
||||
officeId: Number.isFinite(officeId) ? officeId : undefined,
|
||||
totalSupplyUsd,
|
||||
forceRefresh,
|
||||
lineId: String(req.query.lineId || '') || undefined,
|
||||
});
|
||||
const status = snapshot.audit.status === 'aligned' ? 200 : snapshot.audit.status === 'unavailable' ? 503 : 409;
|
||||
res.status(status).json(snapshot);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
res.status(500).json({ error: msg });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/omnl/ho-reserve-coverage', omnlSensitiveRouteGuard, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const officeId = req.query.officeId != null ? Number(req.query.officeId) : undefined;
|
||||
const forceRefresh = String(req.query.refresh || '') === '1';
|
||||
const snapshot = await getCachedHoReserveCoverageSnapshot({
|
||||
officeId: Number.isFinite(officeId) ? officeId : undefined,
|
||||
forceRefresh,
|
||||
});
|
||||
const status = snapshot.status === 'sufficient' ? 200 : snapshot.status === 'unavailable' ? 503 : 409;
|
||||
res.status(status).json(snapshot);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
res.status(500).json({ error: msg });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/omnl/disclosures/ifrs7', omnlSensitiveRouteGuard, async (req: Request, res: Response) => {
|
||||
try {
|
||||
res.json(await buildIfrs7Disclosure(String(req.query.lineId || '') || undefined));
|
||||
|
||||
@@ -26,6 +26,17 @@ import {
|
||||
import { fetchBlockscoutTokenMeta } from '../../services/blockscout-token-meta';
|
||||
import { refreshChain138LiveReferencePrices } from '../../services/chain138-onchain-reference-prices';
|
||||
import { MAX_SINGLE_POOL_TVL_USD, resolveVisibleTokenLiquidityUsd } from '../../services/token-visible-liquidity';
|
||||
import { resolveTokenCashLiquidityUsd } from '../../services/token-cash-liquidity';
|
||||
import { isChain138CashUsdTokenAddress } from '../../config/chain138-cash-tokens';
|
||||
import { isChain138IssuedCStarTokenAddress } from '../../config/chain138-gru-cstar-tokens';
|
||||
import {
|
||||
evaluateSupplyNostroMatch,
|
||||
getCachedCashInBankAuditSnapshot,
|
||||
mergeCashInBankAuditWithSupplyMatch,
|
||||
type CashInBankAuditInfo,
|
||||
type SupplyNostroMatchInfo,
|
||||
} from '../../services/omnl-cash-in-bank-audit';
|
||||
import { getCachedHoReserveCoverageSnapshot, type HoReserveCoverageInfo } from '../../services/chain138-ho-reserve-coverage';
|
||||
|
||||
const router: Router = Router();
|
||||
const tokenRepo = new TokenRepository();
|
||||
@@ -280,6 +291,12 @@ export interface TokenMarketSnapshot {
|
||||
decimals?: number;
|
||||
priceUsd?: number;
|
||||
liquidityUsd?: number;
|
||||
cashLiquidityUsd?: number;
|
||||
cashInBankUsd?: number;
|
||||
cashInBankAudit?: CashInBankAuditInfo;
|
||||
totalSupplyUsd?: number;
|
||||
supplyNostroMatch?: SupplyNostroMatchInfo;
|
||||
hoReserveCoverage?: HoReserveCoverageInfo;
|
||||
volume24h?: number;
|
||||
lastUpdated?: string;
|
||||
source?: string;
|
||||
@@ -436,6 +453,64 @@ async function resolveTokenMarketSnapshot(
|
||||
poolLiquidityUsd,
|
||||
);
|
||||
const liquidityUsd = bestLiquidityUsd > 0 ? bestLiquidityUsd : undefined;
|
||||
const cashLiquidityUsdRaw = chainId === 138
|
||||
? await resolveTokenCashLiquidityUsd(chainId, normalizedAddress)
|
||||
: 0;
|
||||
const cashLiquidityUsd = cashLiquidityUsdRaw > 0 ? cashLiquidityUsdRaw : undefined;
|
||||
|
||||
let cashInBankUsd: number | undefined;
|
||||
let cashInBankAudit: CashInBankAuditInfo | undefined;
|
||||
let totalSupplyUsd: number | undefined;
|
||||
let supplyNostroMatch: SupplyNostroMatchInfo | undefined;
|
||||
let hoReserveCoverage: HoReserveCoverageInfo | undefined;
|
||||
const supplyUnits = decimalStringToNumber(token.totalSupply, token.decimals);
|
||||
const pegPriceUsd = priceUsd != null && priceUsd > 0 ? priceUsd : 1;
|
||||
if (supplyUnits != null && supplyUnits > 0) {
|
||||
totalSupplyUsd = supplyUnits * pegPriceUsd;
|
||||
}
|
||||
if (chainId === 138 && isChain138IssuedCStarTokenAddress(normalizedAddress)) {
|
||||
try {
|
||||
const [bankSnapshot, reserveSnapshot] = await Promise.all([
|
||||
getCachedCashInBankAuditSnapshot({ skipTripleState: true }),
|
||||
getCachedHoReserveCoverageSnapshot(),
|
||||
]);
|
||||
if (bankSnapshot.cashInBankUsd > 0) {
|
||||
cashInBankUsd = bankSnapshot.cashInBankUsd;
|
||||
}
|
||||
supplyNostroMatch = evaluateSupplyNostroMatch({
|
||||
totalSupplyUsd,
|
||||
externalNostroCashUsd: bankSnapshot.cashInBankUsd,
|
||||
headOfficeId: bankSnapshot.officeId,
|
||||
});
|
||||
cashInBankAudit = mergeCashInBankAuditWithSupplyMatch(bankSnapshot.audit, supplyNostroMatch);
|
||||
hoReserveCoverage = reserveSnapshot;
|
||||
} catch (error) {
|
||||
logger.warn('HO liquidity snapshot unavailable', {
|
||||
chainId,
|
||||
address: normalizedAddress,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
} else if (chainId === 138 && isChain138CashUsdTokenAddress(normalizedAddress)) {
|
||||
try {
|
||||
const bankSnapshot = await getCachedCashInBankAuditSnapshot({ skipTripleState: true });
|
||||
if (bankSnapshot.cashInBankUsd > 0) {
|
||||
cashInBankUsd = bankSnapshot.cashInBankUsd;
|
||||
}
|
||||
supplyNostroMatch = evaluateSupplyNostroMatch({
|
||||
totalSupplyUsd,
|
||||
externalNostroCashUsd: bankSnapshot.cashInBankUsd,
|
||||
headOfficeId: bankSnapshot.officeId,
|
||||
});
|
||||
cashInBankAudit = mergeCashInBankAuditWithSupplyMatch(bankSnapshot.audit, supplyNostroMatch);
|
||||
} catch (error) {
|
||||
logger.warn('Cash-in-bank audit snapshot unavailable', {
|
||||
chainId,
|
||||
address: normalizedAddress,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
address: normalizedAddress,
|
||||
@@ -444,12 +519,23 @@ async function resolveTokenMarketSnapshot(
|
||||
decimals: token.decimals,
|
||||
priceUsd: priceUsd != null && priceUsd > 0 ? priceUsd : undefined,
|
||||
liquidityUsd,
|
||||
cashLiquidityUsd,
|
||||
cashInBankUsd,
|
||||
cashInBankAudit,
|
||||
totalSupplyUsd,
|
||||
supplyNostroMatch,
|
||||
hoReserveCoverage,
|
||||
volume24h: market?.volume24h,
|
||||
lastUpdated: market?.lastUpdated instanceof Date
|
||||
? market.lastUpdated.toISOString()
|
||||
: market?.lastUpdated
|
||||
? new Date(market.lastUpdated).toISOString()
|
||||
: undefined,
|
||||
lastUpdated: (() => {
|
||||
const marketLastUpdated = market?.lastUpdated instanceof Date
|
||||
? market.lastUpdated.toISOString()
|
||||
: market?.lastUpdated
|
||||
? new Date(market.lastUpdated).toISOString()
|
||||
: undefined;
|
||||
const auditAt = hoReserveCoverage?.generatedAt;
|
||||
const candidates = [auditAt, marketLastUpdated].filter(Boolean) as string[];
|
||||
return candidates.length > 0 ? candidates.sort().reverse()[0] : undefined;
|
||||
})(),
|
||||
source: pricing?.sourceLayer || (blockscoutMeta?.exchangeRate != null ? 'blockscout' : undefined),
|
||||
blockscoutExchangeRate: blockscoutMeta?.exchangeRate,
|
||||
pricingKind,
|
||||
|
||||
@@ -27,6 +27,7 @@ import omnlComplianceRoutes from './routes/omnl-compliance-routes';
|
||||
import omnlTerminalRoutes from './routes/omnl-terminal-routes';
|
||||
import checkpointRoutes from './routes/checkpoint';
|
||||
import reserveCapacityRoutes from './routes/reserve-capacity';
|
||||
import hoLiquidityRoutes from './routes/ho-liquidity-routes';
|
||||
import metamaskPriceRoutes from './routes/metamask-prices';
|
||||
import { MultiChainIndexer } from '../indexer/chain-indexer';
|
||||
import { OmnlEventPoller } from '../indexer/omnl-event-poller';
|
||||
@@ -296,6 +297,7 @@ export class ApiServer {
|
||||
this.app.use('/api/v1', partnerPayloadRoutes);
|
||||
this.app.use('/api/v1', checkpointRoutes);
|
||||
this.app.use('/api/v1', reserveCapacityRoutes);
|
||||
this.app.use('/api/v1', hoLiquidityRoutes);
|
||||
this.app.use('/api/v1/prices/metamask', metamaskPriceRoutes);
|
||||
this.app.use('/api/v1', omnlComplianceRoutes);
|
||||
this.app.use('/api/v1', omnlTerminalRoutes);
|
||||
|
||||
Reference in New Issue
Block a user