feat(settlement): wire real Chain 138 contracts for 1,000 BTC ledger
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 52s
CI/CD Pipeline / Security Scanning (push) Successful in 2m40s
CI/CD Pipeline / Lint and Format (push) Failing after 49s
CI/CD Pipeline / Terraform Validation (push) Failing after 24s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 27s
Deploy ChainID 138 / Deploy ChainID 138 (push) Has been cancelled
Validation / validate-genesis (push) Has been cancelled
Validation / validate-terraform (push) Has been cancelled
Validation / validate-kubernetes (push) Has been cancelled
Verify Deployment / Verify Deployment (push) Has been cancelled
Validation / validate-smart-contracts (push) Failing after 14s
Validation / validate-security (push) Failing after 1m20s
Validation / validate-documentation (push) Failing after 18s

Add canonical btc-l1-ledger config, /btc/contracts API, Python chain138-oracle
service, and production deploy scripts so secure.omdnl.org can serve live cBTC,
DODO, and PMM pool addresses with headroom-aware mint gating.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-21 02:26:20 -07:00
parent 4e04d9ca51
commit 54ca9bea3b
54 changed files with 2895 additions and 82 deletions

View File

@@ -1,7 +1,64 @@
import axios from 'axios';
import { createHmac } from 'crypto';
import { loadConfig } from '../config';
import { loadHybxConfig } from '@dbis/integration-foundation';
/** RFC1918 / loopback hosts — not reachable from public VPS deploys (e.g. secure.omdnl.org). */
function isPrivateSidecarHost(url: string): boolean {
try {
const host = new URL(url).hostname.toLowerCase();
if (host === 'localhost' || host.endsWith('.local')) return true;
if (/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host)) return true;
const m = /^172\.(\d+)\./.exec(host);
if (m) {
const second = parseInt(m[1], 10);
if (second >= 16 && second <= 31) return true;
}
return false;
} catch {
return false;
}
}
/** Resolve outbound payment URL — public HYBX RTGS API, not LAN sidecar unless explicitly allowed. */
export function resolveHybxPaymentsUrl(baseUrl: string): string {
const explicit = process.env.HYBX_PAYMENTS_URL?.trim();
if (explicit) return explicit.replace(/\/$/, '');
const hybxApi = process.env.HYBX_API?.trim().replace(/\/$/, '');
if (hybxApi) return `${hybxApi}/rtgs/payments`;
const sidecar = process.env.HYBX_MIFOS_FINERACT_SIDECAR_URL?.trim();
const allowLan = process.env.HYBX_ALLOW_LAN_SIDECAR === '1';
if (sidecar && (allowLan || !isPrivateSidecarHost(sidecar))) {
return `${sidecar.replace(/\/$/, '')}/v1/rtgs/payments`;
}
return `${baseUrl.replace(/\/$/, '')}/v1/rtgs/payments`;
}
function hybxWebhookSecret(): string {
return (
process.env.HYBX_WEBHOOK_SECRET?.trim() ||
process.env.HYBX_SIGNING_SECRET?.trim() ||
''
);
}
/** HYBX RTGS API: X-API-Key + X-Timestamp + X-Signature = HMAC-SHA256(secret, raw JSON body). */
export function buildHybxRtgsHeaders(bodyJson: string, apiKey: string): Record<string, string> {
const secret = hybxWebhookSecret();
if (!secret) {
throw new Error('HYBX_WEBHOOK_SECRET required for signed RTGS dispatch');
}
const timestamp = Math.floor(Date.now() / 1000).toString();
return {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
'X-Timestamp': timestamp,
'X-Signature': createHmac('sha256', secret).update(bodyJson).digest('hex'),
};
}
/** Production HYBX rail client for real settlement dispatch */
export class HybxProductionRail {
private readonly baseUrl: string;
@@ -18,7 +75,8 @@ export class HybxProductionRail {
throw new Error(
'HYBX production requires SETTLEMENT_ALLOW_HYBX_PRODUCTION=1 or production.allowHybxProduction in config',
);
} this.baseUrl = cfg.baseUrl.replace(/\/$/, '');
}
this.baseUrl = cfg.baseUrl.replace(/\/$/, '');
this.apiKey = cfg.apiKey;
this.clientSecret = cfg.clientSecret;
}
@@ -32,35 +90,50 @@ export class HybxProductionRail {
beneficiaryName: string;
remittanceInfo: string;
rail: string;
debtorIban?: string;
}): Promise<{ paymentId: string; status: string }> {
const sidecar = process.env.HYBX_MIFOS_FINERACT_SIDECAR_URL?.replace(/\/$/, '');
const url = sidecar ? `${sidecar}/v1/rtgs/payments` : `${this.baseUrl}/v1/payments`;
const { data } = await axios.post(
url,
{
externalId: payload.idempotencyKey,
amount: payload.amount,
currency: payload.currency,
beneficiary: {
iban: payload.creditorIban,
bic: payload.creditorBic,
name: payload.beneficiaryName,
},
remittanceInformation: payload.remittanceInfo,
rail: payload.rail,
const url = resolveHybxPaymentsUrl(this.baseUrl);
const body: Record<string, unknown> = {
externalId: payload.idempotencyKey,
amount: payload.amount,
currency: payload.currency,
beneficiary: {
iban: payload.creditorIban,
bic: payload.creditorBic,
name: payload.beneficiaryName,
},
{
headers: {
remittanceInformation: payload.remittanceInfo,
rail: payload.rail,
};
if (payload.debtorIban) body.debtorIban = payload.debtorIban;
const bodyJson = JSON.stringify(body);
const useRtgsSignedApi = Boolean(process.env.HYBX_API?.trim() || process.env.HYBX_PAYMENTS_URL?.trim());
const headers = useRtgsSignedApi
? buildHybxRtgsHeaders(bodyJson, this.apiKey)
: {
'Content-Type': 'application/json',
'X-API-Key': this.apiKey,
Authorization: `Bearer ${this.clientSecret}`,
},
timeout: 120000,
},
);
return {
paymentId: String(data.paymentId ?? data.id ?? payload.idempotencyKey),
status: String(data.status ?? 'DISPATCHED'),
};
};
if (useRtgsSignedApi && !hybxWebhookSecret()) {
throw new Error('HYBX_WEBHOOK_SECRET required for signed RTGS dispatch (HYBX_API set)');
}
try {
const { data } = await axios.post(url, bodyJson, { headers, timeout: 120000 });
return {
paymentId: String(data.paymentId ?? data.id ?? payload.idempotencyKey),
status: String(data.status ?? 'DISPATCHED'),
};
} catch (e) {
const ax = e as { response?: { status?: number; data?: unknown }; message?: string };
const detail =
ax.response?.data != null
? JSON.stringify(ax.response.data).slice(0, 400)
: ax.message ?? String(e);
throw new Error(`HYBX payment failed (${url}): ${detail}`);
}
}
}

View File

@@ -1,4 +1,5 @@
import axios from 'axios';
import { resolveTokenAggregationBase } from './price-service';
export async function archiveIso20022(payload: {
messageType: string;
@@ -6,7 +7,7 @@ export async function archiveIso20022(payload: {
raw: string;
settlementRef: string;
}): Promise<{ messageId: string }> {
const base = (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, '');
const base = resolveTokenAggregationBase();
const key = process.env.OMNL_API_KEY || '';
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (key) headers.Authorization = `Bearer ${key}`;
@@ -33,7 +34,7 @@ export async function requestTokenMint(payload: {
symbol?: string;
tokenAddress?: string;
}): Promise<{ txHash?: string; status: string }> {
const base = (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, '');
const base = resolveTokenAggregationBase();
const key = process.env.OMNL_API_KEY || '';
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (key) headers.Authorization = `Bearer ${key}`;
@@ -66,7 +67,7 @@ export async function requestTokenTransfer(payload: {
dryRun: boolean;
symbol?: string;
}): Promise<{ txHash?: string; status: string }> {
const base = (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, '');
const base = resolveTokenAggregationBase();
const key = process.env.OMNL_API_KEY || '';
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (key) headers.Authorization = `Bearer ${key}`;
@@ -117,7 +118,7 @@ export async function requestFiatLpToLiquidity(payload: {
settlementRef: string;
dryRun: boolean;
}): Promise<FiatLpLiquidityAdapterResult> {
const base = (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, '');
const base = resolveTokenAggregationBase();
const key = process.env.OMNL_API_KEY || '';
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (key) headers.Authorization = `Bearer ${key}`;

View File

@@ -0,0 +1,15 @@
/**
* Chain 138 spot-price / oracle HTTP base.
* Pricing uses CHAIN138_ORACLE_URL (Python oracle, port 3500) when set;
* falls back to TOKEN_AGGREGATION_URL for gradual cutover.
*/
export function resolveChain138OracleBase(): string {
const oracle = (process.env.CHAIN138_ORACLE_URL || process.env.ORACLE_PUBLISHER_URL || '').replace(/\/$/, '');
if (oracle) return oracle;
return (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3500').replace(/\/$/, '');
}
/** token-aggregation base for OMNL mint/transfer/ISO20022 (not spot prices). */
export function resolveTokenAggregationBase(): string {
return (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, '');
}

View File

@@ -1,6 +1,7 @@
import axios from 'axios';
import { resolveChain138OracleBase } from './price-service';
import { loadBtcLedgerConfig } from '../btc-ledger-config';
const CBTC_ADDRESS = '0xe94260c555ac1d9d3cc9e1632883452ebdf0082e';
const CHAIN_138 = 138;
export type LiveBtcUsdQuote = {
@@ -10,8 +11,12 @@ export type LiveBtcUsdQuote = {
tokenAddress: string;
};
function tokenAggregationBase(): string {
return (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, '');
function cbtcAddress(): string {
try {
return loadBtcLedgerConfig().token.address.toLowerCase();
} catch {
return '0xe94260c555ac1d9d3cc9e1632883452ebdf0082e';
}
}
/**
@@ -19,14 +24,14 @@ function tokenAggregationBase(): string {
* production must not fall back to the $90k repo snapshot.
*/
export async function resolveLiveBtcUsd(): Promise<LiveBtcUsdQuote> {
const base = tokenAggregationBase();
const base = resolveChain138OracleBase();
const key = process.env.OMNL_API_KEY || '';
const headers: Record<string, string> = { Accept: 'application/json' };
if (key) headers.Authorization = `Bearer ${key}`;
const url =
`${base}/api/v1/prices/metamask/v2/chains/${CHAIN_138}/spot-prices` +
`?tokenAddresses=${CBTC_ADDRESS}&vsCurrency=usd`;
`?tokenAddresses=${cbtcAddress()}&vsCurrency=usd`;
let data: unknown;
try {
@@ -49,13 +54,14 @@ export async function resolveLiveBtcUsd(): Promise<LiveBtcUsdQuote> {
throw err;
}
const map = (data ?? {}) as Record<string, Record<string, number>>;
const row = map[CBTC_ADDRESS.toLowerCase()] ?? map[CBTC_ADDRESS];
const addr = cbtcAddress();
const row = map[addr] ?? map[addr.toLowerCase()];
const rate = row?.usd ?? row?.USD;
if (!(typeof rate === 'number' && Number.isFinite(rate) && rate > 0)) {
throw new Error(
'Live BTC/USD price unresolved — settlement blocked (OMNL_REQUIRE_LIVE_BTC_PRICE). ' +
'Ensure token-aggregation FX refresh / CoinGecko is reachable.',
'Ensure chain138-oracle (CHAIN138_ORACLE_URL) or token-aggregation FX refresh is reachable.',
);
}
@@ -79,8 +85,8 @@ export async function resolveLiveBtcUsd(): Promise<LiveBtcUsdQuote> {
return {
btcUsdRate: rate,
source: 'token-aggregation/metamask-spot-prices',
source: 'chain138-oracle/metamask-spot-prices',
asOf: new Date().toISOString(),
tokenAddress: CBTC_ADDRESS,
tokenAddress: addr,
};
}

View File

@@ -29,6 +29,7 @@ import {
} from '../../workflows/btc-l1-settlement';
import { fetchGlBalances, fineractOfficeReachable } from '../../adapters/fineract';
import { resolveLiveBtcUsd } from '../../adapters/pricing';
import { btcLedgerContractsPayload, loadBtcLedgerConfig } from '../../btc-ledger-config';
import {
dispatchExternalBankOrExchange,
getExternalRailsStatus,
@@ -670,9 +671,19 @@ export function createSettlementRouter(): Router {
res.json({ count: rows.length, settlements: rows });
});
router.get('/btc/contracts', (_req, res) => {
if (!requireApiKey(_req, res)) return;
try {
res.json(btcLedgerContractsPayload());
} catch (e) {
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
}
});
router.get('/btc/ledger', async (_req, res) => {
if (!requireApiKey(_req, res)) return;
try {
const btcCfg = loadBtcLedgerConfig();
const [balancesHo, balancesO24, quote] = await Promise.all([
fetchGlBalances(1),
fetchGlBalances(officeId),
@@ -690,6 +701,10 @@ export function createSettlementRouter(): Router {
.filter((r) => r.phase === 'SETTLED')
.reduce((sum, r) => sum + (r.btcSettlement?.amountSats ?? 0), 0);
const m2 = parseFloat(balancesO24['2200'] ?? '0');
const m3 = parseFloat(balancesO24['2300'] ?? '0');
const headroomUsd = m2 - m3;
res.json({
asOf: new Date().toISOString(),
gl: {
@@ -698,6 +713,16 @@ export function createSettlementRouter(): Router {
m2Broad2200: balancesO24['2200'] ?? '0',
m3Token2300: balancesO24['2300'] ?? '0',
},
headroomUsd: headroomUsd.toFixed(2),
requiredHeadroomUsd: btcCfg.requiredHeadroomUsd,
mint1000BtcAllowed: headroomUsd >= btcCfg.requiredHeadroomUsd,
contracts: {
cbtc: btcCfg.token.address,
lineId: btcCfg.token.lineId,
primaryRecipient: btcCfg.recipients.primaryMintTarget,
dodoPmmIntegration: btcCfg.dodoEvm.pmmIntegration,
pmmPools: btcCfg.pmmPools,
},
btcUsd:
'btcUsdRate' in quote
? { rate: quote.btcUsdRate, source: quote.source, asOf: quote.asOf }

View File

@@ -0,0 +1,128 @@
import fs from 'fs';
import path from 'path';
export type BtcLedgerConfig = {
version: string;
chainId: number;
officeId: number;
amountBtc: number;
amountSats: number;
btcUsdPolicyRate: number;
requiredHeadroomUsd: number;
token: {
symbol: string;
address: string;
decimals: number;
omnlLine: string;
lineId: string;
};
recipients: {
primaryMintTarget: string;
existingMintHolder1000: string;
};
fineractGl: Record<string, { glCode: string; officeId: number; memo?: string }>;
oracle: Record<string, string>;
dodoEvm: Record<string, string>;
pmmPools: Record<string, string>;
quoteTokens: Record<string, string>;
rpc: { envVar: string; public: string; explorer: string };
settlementApi: Record<string, string>;
};
let cached: BtcLedgerConfig | null = null;
function repoRoot(): string {
return path.resolve(__dirname, '../../../');
}
function configPath(): string {
const rel =
process.env.OMNL_BTC_LEDGER_CONFIG ||
process.env.BTC_L1_LEDGER_CONFIG ||
'config/btc-l1-ledger-1000.chain138.v1.json';
return path.isAbsolute(rel) ? rel : path.join(repoRoot(), rel.replace(/^\//, ''));
}
export function loadBtcLedgerConfig(): BtcLedgerConfig {
if (cached) return cached;
const raw = JSON.parse(fs.readFileSync(configPath(), 'utf8')) as BtcLedgerConfig;
cached = applyEnvOverrides(raw);
return cached;
}
function envAddr(key: string, fallback: string): string {
const v = process.env[key]?.trim();
return v || fallback;
}
/** Env overrides for deployment without editing JSON. */
function applyEnvOverrides(cfg: BtcLedgerConfig): BtcLedgerConfig {
return {
...cfg,
token: {
...cfg.token,
address: envAddr('CBTC_ADDRESS_138', cfg.token.address),
lineId: process.env.OMNL_BTC_LINE_ID?.trim() || cfg.token.lineId,
},
recipients: {
primaryMintTarget: envAddr(
'BTC_L1_RECIPIENT_ADDRESS',
envAddr('BTC_E2E_RECIPIENT', cfg.recipients.primaryMintTarget),
),
existingMintHolder1000: envAddr(
'BTC_EXISTING_MINT_HOLDER',
cfg.recipients.existingMintHolder1000,
),
},
oracle: {
...cfg.oracle,
aggregator: envAddr('AGGREGATOR_ADDRESS', envAddr('ORACLE_AGGREGATOR_ADDRESS', cfg.oracle.aggregator)),
proxy: envAddr('ORACLE_PROXY_ADDRESS', cfg.oracle.proxy),
oraclePriceFeed: envAddr('ORACLE_PRICE_FEED', cfg.oracle.oraclePriceFeed),
reserveSystem: envAddr('RESERVE_SYSTEM', cfg.oracle.reserveSystem),
priceFeedKeeper: envAddr('PRICE_FEED_KEEPER_ADDRESS', cfg.oracle.priceFeedKeeper),
},
dodoEvm: {
...cfg.dodoEvm,
pmmIntegration: envAddr(
'DODO_PMM_INTEGRATION',
envAddr('CHAIN_138_DODO_PMM_INTEGRATION', cfg.dodoEvm.pmmIntegration),
),
},
pmmPools: {
cBTC_cUSDT: envAddr('CHAIN138_POOL_CBTC_CUSDT', cfg.pmmPools.cBTC_cUSDT),
cBTC_cUSDC: envAddr('CHAIN138_POOL_CBTC_CUSDC', cfg.pmmPools.cBTC_cUSDC),
cBTC_cXAUC: envAddr('CHAIN138_POOL_CBTC_CXAUC', cfg.pmmPools.cBTC_cXAUC),
},
};
}
export function btcLedgerContractsPayload(): Record<string, unknown> {
const cfg = loadBtcLedgerConfig();
return {
configPath: configPath(),
chainId: cfg.chainId,
officeId: cfg.officeId,
amountBtc: cfg.amountBtc,
amountSats: cfg.amountSats,
requiredHeadroomUsd: cfg.requiredHeadroomUsd,
token: cfg.token,
recipients: cfg.recipients,
fineractGl: cfg.fineractGl,
oracle: cfg.oracle,
dodoEvm: cfg.dodoEvm,
pmmPools: cfg.pmmPools,
quoteTokens: cfg.quoteTokens,
rpc: cfg.rpc,
explorerLinks: {
cbtc: `${cfg.rpc.explorer}/address/${cfg.token.address}`,
pmmIntegration: `${cfg.rpc.explorer}/address/${cfg.dodoEvm.pmmIntegration}`,
primaryRecipient: `${cfg.rpc.explorer}/address/${cfg.recipients.primaryMintTarget}`,
poolCbtcCusdc: `${cfg.rpc.explorer}/address/${cfg.pmmPools.cBTC_cUSDC}`,
},
};
}
export function clearBtcLedgerConfigCache(): void {
cached = null;
}

View File

@@ -42,7 +42,7 @@ export type MiddlewareConfig = {
defaultCurrency: string;
rails: {
swift: { enabled: boolean; listenerUrl: string };
hybx: { enabled: boolean; sidecarUrl: string };
hybx: { enabled: boolean; sidecarUrl?: string; publicBaseUrl?: string };
chain138: { enabled: boolean; rpcEnv: string; defaultLineId: string };
multiChain?: {
enabled: boolean;

View File

@@ -21,6 +21,7 @@ import {
} from '../adapters/fineract';
import { requestTokenMint } from '../adapters/omnl';
import { resolveLiveBtcUsd } from '../adapters/pricing';
import { loadBtcLedgerConfig } from '../btc-ledger-config';
import { settlementStore } from '../store/settlement-store';
const MIN_CONFIRMATIONS = Number(process.env.BTC_CONFIRMATIONS_REQUIRED || 6);
@@ -214,13 +215,15 @@ export async function processBtcL1Settlement(
advance('ISO20022_ARCHIVED');
// 3) Mint cBTC in satoshi-accurate BTC decimal amount
const btcLedger = loadBtcLedgerConfig();
const mint = await requestTokenMint({
lineId: 'BTC-M2',
lineId: btcLedger.token.lineId,
amount: btcAmount,
recipient: req.recipientAddress,
settlementRef: record.settlementId,
dryRun: !allowChainMintExecute(),
symbol: 'cBTC',
symbol: btcLedger.token.symbol,
tokenAddress: btcLedger.token.address,
});
advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash });
advance('SETTLED');

View File

@@ -97,6 +97,7 @@ export async function processExternalTransfer(
beneficiaryName: req.beneficiaryName,
remittanceInfo: req.remittanceInfo,
rail: req.rail,
debtorIban: req.debtorIban,
});
advance('HYBX_RAIL_DISPATCHED', { hybxPaymentId: pay.paymentId });
} else {

View File

@@ -168,6 +168,7 @@ export async function processZBankM0ToM4(input: ZBankM0M4Request): Promise<Settl
beneficiaryName: req.beneficiaryName,
remittanceInfo: req.remittanceInfo,
rail: req.rail,
debtorIban: req.debtorIban,
});
advance('HYBX_RAIL_DISPATCHED', { hybxPaymentId: pay.paymentId });
} else {