feat: wire production settlement rails for M2 mint, transfers, and bank SWIFT
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m31s
CI/CD Pipeline / Security Scanning (push) Successful in 3m4s
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
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 44s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 36s
Validation / validate-genesis (push) Successful in 30s
Validation / validate-terraform (push) Failing after 30s
Validation / validate-kubernetes (push) Failing after 10s
Validation / validate-smart-contracts (push) Failing after 12s
Validation / validate-security (push) Failing after 1m52s
Validation / validate-documentation (push) Failing after 18s
Verify Deployment / Verify Deployment (push) Failing after 57s
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m31s
CI/CD Pipeline / Security Scanning (push) Successful in 3m4s
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
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 44s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 36s
Validation / validate-genesis (push) Successful in 30s
Validation / validate-terraform (push) Failing after 30s
Validation / validate-kubernetes (push) Failing after 10s
Validation / validate-smart-contracts (push) Failing after 12s
Validation / validate-security (push) Failing after 1m52s
Validation / validate-documentation (push) Failing after 18s
Verify Deployment / Verify Deployment (push) Failing after 57s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,11 @@ import { computeOmnlReconcileAnchor } from '../../services/omnl-reconcile-anchor
|
||||
import { getOmnlIntegrationStatus } from '../../services/omnl-integration-status';
|
||||
import { getOmnlApiCatalog } from '../../services/omnl-api-catalog';
|
||||
import omnlOpenApi from '../../resources/omnl-openapi.json';
|
||||
import {
|
||||
executeErc20Transfer,
|
||||
executeM2TokenMint,
|
||||
chainRailConfigured,
|
||||
} from '../../services/omnl-settlement-chain';
|
||||
|
||||
const router = Router();
|
||||
router.use(omnlRateLimiter);
|
||||
@@ -329,7 +334,7 @@ router.get('/omnl/health', async (req: Request, res: Response) => {
|
||||
/**
|
||||
* POST /omnl/settlement/token-load — M2 fiat-backed token mint (Office 24 settlement).
|
||||
*/
|
||||
router.post('/omnl/settlement/token-load', (req: Request, res: Response) => {
|
||||
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;
|
||||
@@ -348,29 +353,127 @@ router.post('/omnl/settlement/token-load', (req: Request, res: Response) => {
|
||||
(process.env.SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE === '1' ||
|
||||
process.env.OMNL_ALLOW_CHAIN_MINT_EXECUTE === '1');
|
||||
const loadId = settlementRef ?? `TL-${Date.now()}`;
|
||||
res.json({
|
||||
status: execute ? 'QUEUED' : 'DRY_RUN',
|
||||
loadId,
|
||||
lineId,
|
||||
amount,
|
||||
recipient,
|
||||
symbol: symbol ?? null,
|
||||
tokenAddress: tokenAddress ?? null,
|
||||
settlementRef: settlementRef ?? null,
|
||||
moneyLayer: 'M2',
|
||||
loadFromGl: '2200',
|
||||
creditGl: '2300',
|
||||
capabilities: {
|
||||
swappable: true,
|
||||
convertible: true,
|
||||
transferableInternal: true,
|
||||
transferableExternal: true,
|
||||
},
|
||||
txHash: execute ? undefined : null,
|
||||
message: execute
|
||||
? 'Token load queued for ComplianceCore mint pipeline (M2 → on-chain)'
|
||||
: 'M2 token load validated — set SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=1 to mint on-chain',
|
||||
});
|
||||
const capabilities = {
|
||||
swappable: true,
|
||||
convertible: true,
|
||||
transferableInternal: true,
|
||||
transferableExternal: true,
|
||||
};
|
||||
|
||||
if (!execute) {
|
||||
res.json({
|
||||
status: 'DRY_RUN',
|
||||
loadId,
|
||||
lineId,
|
||||
amount,
|
||||
recipient,
|
||||
symbol: symbol ?? null,
|
||||
tokenAddress: tokenAddress ?? null,
|
||||
settlementRef: settlementRef ?? null,
|
||||
moneyLayer: 'M2',
|
||||
loadFromGl: '2200',
|
||||
creditGl: '2300',
|
||||
capabilities,
|
||||
txHash: null,
|
||||
chainRailConfigured: chainRailConfigured(),
|
||||
message: 'M2 token load validated — set SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=1 to mint on-chain',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tokenAddress?.startsWith('0x')) {
|
||||
res.status(400).json({
|
||||
error: 'tokenAddress required for on-chain mint',
|
||||
status: 'QUEUED',
|
||||
loadId,
|
||||
capabilities,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { txHash } = await executeM2TokenMint({
|
||||
tokenAddress,
|
||||
recipient,
|
||||
amount,
|
||||
});
|
||||
res.json({
|
||||
status: 'SETTLED',
|
||||
loadId,
|
||||
lineId,
|
||||
amount,
|
||||
recipient,
|
||||
symbol: symbol ?? null,
|
||||
tokenAddress,
|
||||
settlementRef: settlementRef ?? null,
|
||||
moneyLayer: 'M2',
|
||||
capabilities,
|
||||
txHash,
|
||||
message: 'M2 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' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Contract, JsonRpcProvider, Wallet, parseUnits } from 'ethers';
|
||||
|
||||
function rpcUrl(): string | undefined {
|
||||
return process.env.RPC_URL_138 || process.env.CHAIN_138_RPC_URL;
|
||||
}
|
||||
|
||||
function operatorKey(): string | undefined {
|
||||
return process.env.OMNL_MINT_OPERATOR_PRIVATE_KEY?.trim();
|
||||
}
|
||||
|
||||
function chainExecuteEnabled(): boolean {
|
||||
return (
|
||||
process.env.SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE === '1' ||
|
||||
process.env.OMNL_ALLOW_CHAIN_MINT_EXECUTE === '1'
|
||||
);
|
||||
}
|
||||
|
||||
async function tokenDecimals(token: Contract, fallback = 18): Promise<number> {
|
||||
try {
|
||||
return Number(await token.decimals());
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/** Mint M2 compliant token to recipient wallet (Chain 138). */
|
||||
export async function executeM2TokenMint(params: {
|
||||
tokenAddress: string;
|
||||
recipient: string;
|
||||
amount: string;
|
||||
decimals?: number;
|
||||
}): Promise<{ txHash: string }> {
|
||||
const pk = operatorKey();
|
||||
const rpc = rpcUrl();
|
||||
if (!chainExecuteEnabled()) {
|
||||
throw new Error('Chain mint 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 for on-chain mint');
|
||||
}
|
||||
if (!params.tokenAddress?.startsWith('0x')) {
|
||||
throw new Error('tokenAddress required for on-chain mint');
|
||||
}
|
||||
|
||||
const provider = new JsonRpcProvider(rpc);
|
||||
const wallet = new Wallet(pk, provider);
|
||||
const abi = [
|
||||
'function mint(address to, uint256 amount)',
|
||||
'function decimals() view returns (uint8)',
|
||||
];
|
||||
const token = new Contract(params.tokenAddress, abi, wallet);
|
||||
const decimals = await tokenDecimals(token, params.decimals ?? 18);
|
||||
const wei = parseUnits(params.amount, decimals);
|
||||
const tx = await token.mint(params.recipient, wei);
|
||||
const receipt = await tx.wait(1);
|
||||
return { txHash: receipt?.hash ?? tx.hash };
|
||||
}
|
||||
|
||||
/** Transfer ERC-20 from operator treasury to recipient (internal / web3 wallet). */
|
||||
export async function executeErc20Transfer(params: {
|
||||
tokenAddress: string;
|
||||
recipient: string;
|
||||
amount: string;
|
||||
decimals?: number;
|
||||
}): Promise<{ txHash: string }> {
|
||||
const pk = operatorKey();
|
||||
const rpc = rpcUrl();
|
||||
if (!chainExecuteEnabled()) {
|
||||
throw new Error('Chain transfer 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 for on-chain transfer');
|
||||
}
|
||||
if (!params.tokenAddress?.startsWith('0x')) {
|
||||
throw new Error('tokenAddress required for on-chain transfer');
|
||||
}
|
||||
|
||||
const provider = new JsonRpcProvider(rpc);
|
||||
const wallet = new Wallet(pk, provider);
|
||||
const abi = [
|
||||
'function transfer(address to, uint256 amount) returns (bool)',
|
||||
'function decimals() view returns (uint8)',
|
||||
];
|
||||
const token = new Contract(params.tokenAddress, abi, wallet);
|
||||
const decimals = await tokenDecimals(token, params.decimals ?? 18);
|
||||
const wei = parseUnits(params.amount, decimals);
|
||||
const tx = await token.transfer(params.recipient, wei);
|
||||
const receipt = await tx.wait(1);
|
||||
return { txHash: receipt?.hash ?? tx.hash };
|
||||
}
|
||||
|
||||
export function chainRailConfigured(): boolean {
|
||||
return Boolean(operatorKey() && rpcUrl() && chainExecuteEnabled());
|
||||
}
|
||||
Reference in New Issue
Block a user