feat(settlement): production BTC L1 rails, hot cBTC liquidity, Dfns/Cobo smoke
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>
This commit is contained in:
2026-07-19 09:26:45 -07:00
parent 286b2a8915
commit 5a9889cace
68 changed files with 7558 additions and 16 deletions

View File

@@ -22,6 +22,11 @@ import {
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';
@@ -522,4 +527,72 @@ router.post('/omnl/settlement/token-transfer', async (req: Request, res: Respons
}
});
/**
* 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;

View File

@@ -195,6 +195,7 @@ export class ApiServer {
const reserveDashboardPath = path.join(__dirname, '../../public/reserve-dashboard.html');
const reserveInstitutionalPath = path.join(__dirname, '../../public/reserve-institutional.html');
const gruVaultControlsPath = path.join(__dirname, '../../public/gru-vault-controls.html');
const cbtcLandingPath = path.join(__dirname, '../../public/cbtc-landing.html');
const authorizeOmnlHtml = (req: Request, res: Response): boolean => {
const tok = process.env.OMNL_DASHBOARD_TOKEN?.trim();
@@ -263,6 +264,17 @@ export class ApiServer {
res.type('html').send(readFileSync(gruVaultControlsPath, 'utf8'));
});
/** Where the 1000 cBTC mint landed (wallets + PMM pools). */
const sendCbtcLanding = (_req: Request, res: Response) => {
if (!existsSync(cbtcLandingPath)) {
res.status(404).type('text/plain').send('cbtc-landing.html missing');
return;
}
res.type('html').send(readFileSync(cbtcLandingPath, 'utf8'));
};
this.app.get('/cbtc', sendCbtcLanding);
this.app.get('/cbtc/landing', sendCbtcLanding);
// Public API catalog (register before routers so GET /api/v1 is not swallowed by middleware-only mounts)
const sendApiV1Catalog = (_req: Request, res: Response) => {
res.json({

View File

@@ -0,0 +1,53 @@
import fs from 'fs';
import path from 'path';
export type FiatLpParityConfig = {
version: string;
chainId: number;
mainnetParityChainId: number;
contracts: {
enhancedSwapRouterV2: string;
officialUsdt: string;
officialUsdc: string;
officialWeth: string;
[key: string]: string;
};
defaultSlippageBps: number;
minSwapAmountRaw: string;
lpPrefix: string;
realTargets: Record<string, string>;
explicitLpToReal: Array<{ lpSymbol: string; realSymbol: string }>;
};
let cached: FiatLpParityConfig | null = null;
export function loadFiatLpParityConfig(): FiatLpParityConfig {
if (cached) return cached;
const configPath =
process.env.CHAIN138_FIAT_LP_CONFIG ||
path.resolve(__dirname, '../../../../config/chain138-fiat-lp-liquidity.v1.json');
cached = JSON.parse(fs.readFileSync(configPath, 'utf8')) as FiatLpParityConfig;
return cached;
}
export function realTokenAddress(realSymbol: string, cfg = loadFiatLpParityConfig()): string {
const sym = realSymbol.toUpperCase();
if (sym === 'USDT') return cfg.contracts.officialUsdt;
if (sym === 'USDC') return cfg.contracts.officialUsdc;
if (sym === 'WETH' || sym === 'ETH') return cfg.contracts.officialWeth;
return cfg.contracts.officialUsdt;
}
export function resolveRealSymbolForLp(lpSymbol: string, currencyCode?: string): string {
const cfg = loadFiatLpParityConfig();
const explicit = cfg.explicitLpToReal.find((e) => e.lpSymbol === lpSymbol);
if (explicit) return explicit.realSymbol;
if (currencyCode && cfg.realTargets[currencyCode]) {
return cfg.realTargets[currencyCode];
}
if (lpSymbol.toUpperCase().includes('USDC')) return 'USDC';
if (lpSymbol.toUpperCase().includes('ETH') || lpSymbol.toUpperCase().includes('BTC')) {
return 'WETH';
}
return 'USDT';
}

View File

@@ -219,6 +219,17 @@ export function resolveCanonicalPriceUsdForSpec(spec: CanonicalTokenSpec): Canon
};
}
const requireLiveBtc =
referenceSymbol === 'BTC' &&
(process.env.OMNL_REQUIRE_LIVE_BTC_PRICE === '1' ||
(process.env.NODE_ENV === 'production' && process.env.OMNL_REQUIRE_LIVE_BTC_PRICE !== '0'));
if (requireLiveBtc) {
return {
referenceSymbol,
source: 'unresolved',
};
}
const fallback = REPO_FALLBACK_PRICE_USD[referenceSymbol];
if (fallback !== undefined) {
return {

View File

@@ -0,0 +1,346 @@
import fs from 'fs';
import path from 'path';
import { formatUnits, parseUnits } from 'ethers';
import {
loadFiatLpParityConfig,
realTokenAddress,
resolveRealSymbolForLp,
} from '../config/fiat-lp-liquidity';
import { BestExecutionPlanner } from './best-execution-planner';
import { InternalExecutionPlanV2Builder } from './internal-execution-plan-v2';
import {
chainRailConfigured,
executeRouterSwap,
readErc20Balance,
} from './omnl-settlement-chain';
const CHAIN_ID = 138;
type M2RegistryToken = {
symbol: string;
address: string;
decimals?: number;
currencyCode?: string;
omnlLine?: string;
swappable?: boolean;
};
type M2TokenRegistryFile = {
tokens: M2RegistryToken[];
};
export type LpPosition = {
symbol: string;
tokenAddress: string;
decimals: number;
balanceRaw: string;
balanceFormatted: string;
realSymbol: string;
realTokenAddress: string;
amountOutQuoted?: string;
swappable: boolean;
};
export type FiatLpConversionResult = {
settlementRef: string;
holder: string;
recipient: string;
dryRun: boolean;
positions: LpPosition[];
swaps: Array<{
lpSymbol: string;
tokenIn: string;
tokenOut: string;
realSymbol: string;
amountIn: string;
amountOutQuoted?: string;
txHash?: string;
status: 'DRY_RUN' | 'QUOTED' | 'SUBMITTED' | 'SETTLED' | 'SKIPPED' | 'FAILED';
error?: string;
}>;
chainRailConfigured: boolean;
};
function loadM2Registry(): M2TokenRegistryFile {
const registryPath =
process.env.OMNL_M2_TOKEN_REGISTRY ||
path.resolve(__dirname, '../../../../config/omnl-m2-token-registry.v1.json');
return JSON.parse(fs.readFileSync(registryPath, 'utf8')) as M2TokenRegistryFile;
}
function isLpToken(symbol: string, cfg = loadFiatLpParityConfig()): boolean {
return symbol.startsWith(cfg.lpPrefix) && symbol.length > cfg.lpPrefix.length;
}
async function quoteAmountOut(
tokenIn: string,
tokenOut: string,
amountInRaw: string,
): Promise<string | undefined> {
try {
const planner = new BestExecutionPlanner();
const result = await planner.plan({
sourceChainId: CHAIN_ID,
destinationChainId: CHAIN_ID,
tokenIn,
tokenOut,
amountIn: amountInRaw,
recipient: '0x0000000000000000000000000000000000000001',
constraints: { allowBridge: false, maxSlippageBps: 50 },
});
const raw = result.estimatedAmountOut ?? result.minAmountOut;
return raw ?? undefined;
} catch {
return undefined;
}
}
/** Read all fiat LP (c*) balances for a holder with quoted real-fund value. */
export async function readFiatLpPositions(holderAddress: string): Promise<LpPosition[]> {
const registry = loadM2Registry();
const cfg = loadFiatLpParityConfig();
const minRaw = BigInt(cfg.minSwapAmountRaw);
const positions: LpPosition[] = [];
for (const token of registry.tokens) {
if (!isLpToken(token.symbol)) continue;
if (!token.address.startsWith('0x') || token.address === '0x0000000000000000000000000000000000000000') {
continue;
}
let balanceRaw = 0n;
try {
balanceRaw = await readErc20Balance({ tokenAddress: token.address, holder: holderAddress });
} catch {
balanceRaw = 0n;
}
const realSymbol = resolveRealSymbolForLp(token.symbol, token.currencyCode);
const realAddr = realTokenAddress(realSymbol);
const balanceFormatted = formatUnits(balanceRaw, token.decimals ?? 18);
let amountOutQuoted: string | undefined;
if (balanceRaw >= minRaw && token.address.toLowerCase() !== realAddr.toLowerCase()) {
const quoted = await quoteAmountOut(token.address, realAddr, balanceRaw.toString());
if (quoted) {
const realDecimals = realSymbol === 'WETH' ? 18 : 6;
amountOutQuoted = formatUnits(quoted, realDecimals);
}
}
positions.push({
symbol: token.symbol,
tokenAddress: token.address,
decimals: token.decimals ?? 18,
balanceRaw: balanceRaw.toString(),
balanceFormatted,
realSymbol,
realTokenAddress: realAddr,
amountOutQuoted,
swappable: Boolean(token.swappable && balanceRaw >= minRaw),
});
}
return positions;
}
/** Convert one or all LP tokens to real USDT/USDC/WETH on Chain 138. */
export async function convertFiatLpToLiquidity(params: {
holderAddress: string;
recipientAddress?: string;
symbol?: string;
amount?: string;
convertAll?: boolean;
maxSlippageBps?: number;
dryRun?: boolean;
settlementRef: string;
}): Promise<FiatLpConversionResult> {
const cfg = loadFiatLpParityConfig();
const registry = loadM2Registry();
const holder = params.holderAddress.trim();
const recipient = (params.recipientAddress ?? holder).trim();
const dryRun = params.dryRun !== false && params.dryRun !== undefined ? params.dryRun : !chainRailConfigured();
const slippage = params.maxSlippageBps ?? cfg.defaultSlippageBps;
const minRaw = BigInt(cfg.minSwapAmountRaw);
const planner = new InternalExecutionPlanV2Builder();
const positions = await readFiatLpPositions(holder);
const swaps: FiatLpConversionResult['swaps'] = [];
const targets = params.convertAll
? positions.filter((p) => p.swappable)
: positions.filter((p) => {
if (!params.symbol) return false;
return p.symbol.toLowerCase() === params.symbol.toLowerCase() && p.swappable;
});
if (!params.convertAll && params.symbol && targets.length === 0) {
const bySym = registry.tokens.find((t) => t.symbol.toLowerCase() === params.symbol!.toLowerCase());
if (!bySym) {
swaps.push({
lpSymbol: params.symbol,
tokenIn: '',
tokenOut: '',
realSymbol: '',
amountIn: params.amount ?? '0',
status: 'FAILED',
error: `Unknown LP symbol ${params.symbol}`,
});
} else {
swaps.push({
lpSymbol: bySym.symbol,
tokenIn: bySym.address,
tokenOut: realTokenAddress(resolveRealSymbolForLp(bySym.symbol, bySym.currencyCode)),
realSymbol: resolveRealSymbolForLp(bySym.symbol, bySym.currencyCode),
amountIn: params.amount ?? '0',
status: 'SKIPPED',
error: 'Balance below minimum or token not swappable',
});
}
}
for (const pos of targets) {
let amountInRaw = BigInt(pos.balanceRaw);
if (params.amount && !params.convertAll) {
try {
amountInRaw = parseUnits(params.amount, pos.decimals);
} catch {
swaps.push({
lpSymbol: pos.symbol,
tokenIn: pos.tokenAddress,
tokenOut: pos.realTokenAddress,
realSymbol: pos.realSymbol,
amountIn: params.amount,
status: 'FAILED',
error: 'Invalid amount',
});
continue;
}
}
if (amountInRaw < minRaw) {
swaps.push({
lpSymbol: pos.symbol,
tokenIn: pos.tokenAddress,
tokenOut: pos.realTokenAddress,
realSymbol: pos.realSymbol,
amountIn: amountInRaw.toString(),
status: 'SKIPPED',
error: 'Below minimum swap amount',
});
continue;
}
if (pos.tokenAddress.toLowerCase() === pos.realTokenAddress.toLowerCase()) {
swaps.push({
lpSymbol: pos.symbol,
tokenIn: pos.tokenAddress,
tokenOut: pos.realTokenAddress,
realSymbol: pos.realSymbol,
amountIn: amountInRaw.toString(),
amountOutQuoted: pos.balanceFormatted,
status: 'SKIPPED',
error: 'Already real fund token',
});
continue;
}
const amountOutQuoted = await quoteAmountOut(
pos.tokenAddress,
pos.realTokenAddress,
amountInRaw.toString(),
);
if (dryRun) {
swaps.push({
lpSymbol: pos.symbol,
tokenIn: pos.tokenAddress,
tokenOut: pos.realTokenAddress,
realSymbol: pos.realSymbol,
amountIn: amountInRaw.toString(),
amountOutQuoted: amountOutQuoted
? formatUnits(amountOutQuoted, pos.realSymbol === 'WETH' ? 18 : 6)
: undefined,
status: 'DRY_RUN',
});
continue;
}
try {
const plan = await planner.build({
sourceChainId: CHAIN_ID,
destinationChainId: CHAIN_ID,
tokenIn: pos.tokenAddress,
tokenOut: pos.realTokenAddress,
amountIn: amountInRaw.toString(),
recipient,
constraints: { maxSlippageBps: slippage, allowBridge: false },
});
if (!plan.execution?.encodedCalldata) {
swaps.push({
lpSymbol: pos.symbol,
tokenIn: pos.tokenAddress,
tokenOut: pos.realTokenAddress,
realSymbol: pos.realSymbol,
amountIn: amountInRaw.toString(),
amountOutQuoted: amountOutQuoted
? formatUnits(amountOutQuoted, pos.realSymbol === 'WETH' ? 18 : 6)
: undefined,
status: 'FAILED',
error: plan.error ?? 'No executable swap route',
});
continue;
}
const { txHash } = await executeRouterSwap({
routerAddress: plan.execution.contractAddress,
calldata: plan.execution.encodedCalldata,
tokenIn: pos.tokenAddress,
amountInRaw: amountInRaw.toString(),
});
swaps.push({
lpSymbol: pos.symbol,
tokenIn: pos.tokenAddress,
tokenOut: pos.realTokenAddress,
realSymbol: pos.realSymbol,
amountIn: amountInRaw.toString(),
amountOutQuoted: amountOutQuoted
? formatUnits(amountOutQuoted, pos.realSymbol === 'WETH' ? 18 : 6)
: undefined,
txHash,
status: 'SETTLED',
});
} catch (err) {
swaps.push({
lpSymbol: pos.symbol,
tokenIn: pos.tokenAddress,
tokenOut: pos.realTokenAddress,
realSymbol: pos.realSymbol,
amountIn: amountInRaw.toString(),
status: 'FAILED',
error: err instanceof Error ? err.message : String(err),
});
}
}
return {
settlementRef: params.settlementRef,
holder,
recipient,
dryRun,
positions,
swaps,
chainRailConfigured: chainRailConfigured(),
};
}
/** Resolve LP token by omnl line id (e.g. USD-M2 → cUSDT). */
export function resolveLpTokenByLineId(lineId: string): { symbol: string; address: string } | undefined {
const registry = loadM2Registry();
const entry =
registry.tokens.find((t) => t.omnlLine === lineId) ??
registry.tokens.find((t) => t.symbol.toLowerCase() === 'cusdt' && lineId.includes('USD'));
if (!entry?.address.startsWith('0x')) return undefined;
return { symbol: entry.symbol, address: entry.address };
}

View File

@@ -4,9 +4,15 @@ import { fetchBlockscoutTokenMeta } from './blockscout-token-meta';
import { evaluateSupplyNostroMatch, type SupplyNostroMatchInfo } from './omnl-cash-in-bank-audit';
import { runTripleStateReconcile, type TripleReconcileReport } from './omnl-triple-reconcile';
import { resolveCanonicalPriceUsd } from './canonical-price-oracle';
import {
fineractConfigured,
fetchOfficeSignedGlBalances,
} from './omnl-fineract-office-gl';
const CHAIN_138 = 138;
const DEFAULT_CUSDC_ADDRESS = '0xf22258f57794CC8E06237084b353Ab30fFfa640b';
const DEFAULT_CBTC_ADDRESS = '0xe94260c555ac1d9d3cc9e1632883452ebdf0082e';
const BTC_L1_CUSTODY_GL = '12015';
export interface HoLiquidityTripleStateSummary {
aligned?: boolean;
@@ -19,6 +25,10 @@ export interface HoLiquidityPerTokenSample {
totalSupplyUsd?: number;
cashInBankUsd?: number;
supplyNostroMatch?: SupplyNostroMatchInfo;
/** Optional GL / pool enrichment for digital-asset reserves */
glCode?: string;
glBalanceUsd?: number;
pmmPoolTvlUsd?: number;
}
function decimalStringToNumber(value?: string, decimals?: number): number | null {
@@ -104,3 +114,75 @@ export async function loadCusdcPerTokenSample(input: {
supplyNostroMatch,
};
}
/** cBTC + GL 12015 L1 BTC custody sample for HO liquidity snapshot. */
export async function loadCbtcBtcReserveSample(input: {
cashInBankUsd: number;
officeId: number;
}): Promise<HoLiquidityPerTokenSample | undefined> {
const address = (process.env.CBTC_ADDRESS_138 || DEFAULT_CBTC_ADDRESS).toLowerCase();
const meta = await fetchBlockscoutTokenMeta(CHAIN_138, address);
const decimals = meta?.decimals ?? 8;
const supplyUnits = decimalStringToNumber(meta?.totalSupply, decimals);
const { priceUsd } = resolveCanonicalPriceUsd(CHAIN_138, address);
const pegPriceUsd = priceUsd != null && priceUsd > 0 ? priceUsd : undefined;
const totalSupplyUsd =
supplyUnits != null && supplyUnits > 0 && pegPriceUsd != null
? supplyUnits * pegPriceUsd
: undefined;
let glBalanceUsd: number | undefined;
try {
if (fineractConfigured()) {
const rows = await fetchOfficeSignedGlBalances(1, [BTC_L1_CUSTODY_GL]);
const row = rows.find((r) => r.glCode === BTC_L1_CUSTODY_GL);
if (row) {
glBalanceUsd = Math.abs(parseFloat(row.amount) || 0);
}
}
} catch {
glBalanceUsd = undefined;
}
let pmmPoolTvlUsd: number | undefined;
try {
const poolEnv =
process.env.CHAIN138_POOL_CBTC_CUSDT ||
process.env.CHAIN138_POOL_CBTC_CUSDC ||
process.env.CHAIN138_POOL_CBTC_CXAUC;
if (poolEnv) {
const { getLiveDodoPools } = await import('./live-dodo-fallback');
const pools = await getLiveDodoPools(CHAIN_138);
const cbtcPools = pools.filter(
(p) =>
String((p as { token0Address?: string }).token0Address || '').toLowerCase() === address ||
String((p as { token1Address?: string }).token1Address || '').toLowerCase() === address ||
String((p as { address?: string }).address || '').toLowerCase() === poolEnv.toLowerCase(),
);
if (cbtcPools.length) {
pmmPoolTvlUsd = cbtcPools.reduce(
(s, p) => s + (Number((p as { totalLiquidityUsd?: number }).totalLiquidityUsd) || 0),
0,
);
}
}
} catch {
pmmPoolTvlUsd = undefined;
}
const supplyNostroMatch = evaluateSupplyNostroMatch({
totalSupplyUsd,
externalNostroCashUsd: glBalanceUsd ?? input.cashInBankUsd,
headOfficeId: input.officeId,
});
return {
address,
totalSupplyUsd,
cashInBankUsd: glBalanceUsd ?? (input.cashInBankUsd > 0 ? input.cashInBankUsd : undefined),
supplyNostroMatch,
glCode: BTC_L1_CUSTODY_GL,
glBalanceUsd,
pmmPoolTvlUsd,
};
}

View File

@@ -9,6 +9,7 @@ import {
import { fineractConfigured } from './omnl-fineract-office-gl';
import {
loadCusdcPerTokenSample,
loadCbtcBtcReserveSample,
loadHoLiquidityTripleStateSummary,
type HoLiquidityPerTokenSample,
type HoLiquidityTripleStateSummary,
@@ -33,6 +34,7 @@ export interface HoLiquiditySnapshot {
aggregateSupplyNostroMatch?: SupplyNostroMatchInfo;
perTokenSamples?: {
cUSDC?: HoLiquidityPerTokenSample;
cBTC?: HoLiquidityPerTokenSample;
};
tripleState?: HoLiquidityTripleStateSummary;
alerts: HoLiquidityAlert[];
@@ -159,14 +161,26 @@ export async function buildHoLiquiditySnapshot(options?: {
),
).toISOString();
const [tripleState, cusdcSample] = await Promise.all([
const [tripleState, cusdcSample, cbtcSample] = await Promise.all([
loadHoLiquidityTripleStateSummary().catch(() => undefined),
loadCusdcPerTokenSample({
cashInBankUsd: cashInBankEnriched.cashInBankUsd,
officeId,
}).catch(() => undefined),
loadCbtcBtcReserveSample({
cashInBankUsd: cashInBankEnriched.cashInBankUsd,
officeId,
}).catch(() => undefined),
]);
const perTokenSamples =
cusdcSample || cbtcSample
? {
...(cusdcSample ? { cUSDC: cusdcSample } : {}),
...(cbtcSample ? { cBTC: cbtcSample } : {}),
}
: undefined;
return {
generatedAt,
officeId,
@@ -176,7 +190,7 @@ export async function buildHoLiquiditySnapshot(options?: {
cashInBank: cashInBankEnriched,
reserveCoverage,
aggregateSupplyNostroMatch,
perTokenSamples: cusdcSample ? { cUSDC: cusdcSample } : undefined,
perTokenSamples,
tripleState,
alerts: evaluateHoLiquidityAlerts({ cashInBank: cashInBankEnriched, reserveCoverage, generatedAt }),
auditMaxAgeMinutes: auditMaxAgeMinutes(),

View File

@@ -92,3 +92,60 @@ export async function executeErc20Transfer(params: {
export function chainRailConfigured(): boolean {
return Boolean(operatorKey() && rpcUrl() && chainExecuteEnabled());
}
/** Read ERC-20 balance (raw units) for holder. */
export async function readErc20Balance(params: {
tokenAddress: string;
holder: string;
}): Promise<bigint> {
const rpc = rpcUrl();
if (!rpc) throw new Error('RPC_URL_138 required');
const provider = new JsonRpcProvider(rpc);
const abi = ['function balanceOf(address) view returns (uint256)'];
const token = new Contract(params.tokenAddress, abi, provider);
return token.balanceOf(params.holder) as Promise<bigint>;
}
/** Approve router and submit swap calldata from EnhancedSwapRouterV2 planner. */
export async function executeRouterSwap(params: {
routerAddress: string;
calldata: string;
tokenIn: string;
amountInRaw: string;
}): Promise<{ txHash: string }> {
const pk = operatorKey();
const rpc = rpcUrl();
if (!chainExecuteEnabled()) {
throw new Error('Chain execute disabled — set SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=1');
}
if (!pk || !rpc) {
throw new Error('OMNL_MINT_OPERATOR_PRIVATE_KEY and RPC_URL_138 required');
}
const provider = new JsonRpcProvider(rpc);
const wallet = new Wallet(pk, provider);
const amountIn = BigInt(params.amountInRaw);
if (params.tokenIn.startsWith('0x') && amountIn > 0n) {
const erc20 = new Contract(
params.tokenIn,
[
'function allowance(address owner, address spender) view returns (uint256)',
'function approve(address spender, uint256 amount) returns (bool)',
],
wallet,
);
const allowance = (await erc20.allowance(wallet.address, params.routerAddress)) as bigint;
if (allowance < amountIn) {
const approveTx = await erc20.approve(params.routerAddress, amountIn);
await approveTx.wait(1);
}
}
const tx = await wallet.sendTransaction({
to: params.routerAddress,
data: params.calldata,
});
const receipt = await tx.wait(1);
return { txHash: receipt?.hash ?? tx.hash };
}