"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.processTransfer = processTransfer; const uuid_1 = require("uuid"); const settlement_core_1 = require("@dbis/settlement-core"); const config_1 = require("../config"); const fineract_1 = require("../adapters/fineract"); const hybx_production_1 = require("../adapters/hybx-production"); const omnl_1 = require("../adapters/omnl"); const swift_1 = require("../adapters/swift"); const settlement_store_1 = require("../store/settlement-store"); const btc_l1_settlement_1 = require("./btc-l1-settlement"); /** * INTERNAL / CHAIN138 → on-chain ERC-20 when execute enabled. * SWIFT / RTGS / REVOLUT / WISE → bank rail (IBAN). * BINANCE → exchange withdraw (address) after ledger journal. * CHAIN138 with externalRecipient → treated as external crypto egress (still on-chain). */ async function processTransfer(input) { const cfg = (0, config_1.loadConfig)(); const profile = (0, config_1.loadOfficeProfile)(); const officeId = (0, config_1.settlementOfficeId)(input.officeId); const req = { ...input, officeId }; const isBankExternal = req.rail === 'SWIFT' || req.rail === 'RTGS' || req.rail === 'REVOLUT' || req.rail === 'WISE'; const isExchangeExternal = req.rail === 'BINANCE'; /** Crypto leaving OMNL custody to any 0x wallet (external transferable). */ const isCryptoExternal = !isBankExternal && !isExchangeExternal && Boolean(req.tokenAddress?.startsWith('0x')) && Boolean(req.recipientAddress?.startsWith('0x')) && (req.rail === 'CHAIN138' || req.remittanceInfo?.includes('EXTERNAL_CRYPTO')); const isExternal = isBankExternal || isCryptoExternal || isExchangeExternal; const existing = settlement_store_1.settlementStore.getByIdempotencyKey(req.idempotencyKey); if (existing?.phase === 'SETTLED') return existing; const now = new Date().toISOString(); let record = existing ?? { settlementId: (0, uuid_1.v4)(), idempotencyKey: req.idempotencyKey, phase: 'RECEIVED', request: { idempotencyKey: req.idempotencyKey, officeId, valueDate: now.slice(0, 10), currency: req.currency ?? (req.tokenSymbol === 'cBTC' ? 'BTC' : 'USD'), amount: req.amount, creditorIban: req.creditorIban ?? (isBankExternal ? '' : isExchangeExternal ? `EXT-BINANCE-${req.recipientAddress || 'withdraw'}` : isCryptoExternal ? `EXT-CRYPTO-${req.recipientAddress}` : 'INTERNAL-CHAIN138'), beneficiaryName: req.beneficiaryName ?? req.recipientAddress, remittanceInfo: req.remittanceInfo ?? (isCryptoExternal ? `EXTERNAL_CRYPTO ${req.tokenSymbol} → ${req.recipientAddress}` : isExchangeExternal ? `BINANCE_WITHDRAW ${req.currency || req.tokenSymbol}` : `Transfer ${req.tokenSymbol} → ${req.recipientAddress}`), moneyLayers: req.moneyLayers.length ? req.moneyLayers : isBankExternal || isExchangeExternal ? ['M2', 'M4'] : isCryptoExternal ? ['M3', 'M4'] : ['M2', 'M3'], rail: req.rail === 'CHAIN138' ? 'CHAIN138' : req.rail === 'INTERNAL' ? 'INTERNAL' : req.rail === 'REVOLUT' || req.rail === 'WISE' || req.rail === 'BINANCE' ? req.rail : 'SWIFT', }, errors: [], createdAt: now, updatedAt: now, }; const advance = (phase, patch = {}) => { record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch }; settlement_store_1.settlementStore.save(record); }; try { if (isBankExternal && !req.creditorIban) { throw new Error('creditorIban required for external bank transfer'); } if (isExchangeExternal && !req.recipientAddress?.startsWith('0x') && !req.recipientAddress) { throw new Error('recipientAddress required for Binance withdraw'); } if ((isCryptoExternal || req.rail === 'INTERNAL' || req.rail === 'CHAIN138') && !req.recipientAddress?.startsWith('0x')) { throw new Error('recipientAddress (0x) required for on-chain transfer'); } advance('VALIDATED'); advance('VERBIAGE_ROLLED', { verbiageDocument: [ `OMNL ${req.rail} transfer · ${req.tokenSymbol}`, `Layers ${record.request.moneyLayers.join('/')} · ${req.amount}`, isBankExternal ? `IBAN ${req.creditorIban} · M4 GL ${profile.settlement.glM4NearMoney ?? '1000'} suspense` : isCryptoExternal ? `EXTERNAL CRYPTO egress → ${req.recipientAddress} (transferableExternal)` : `To ${req.recipientAddress} · internal chain`, ].join('\n'), }); const amount = parseFloat(req.amount) || 0; if (amount > 0 && isBankExternal) { const layers = record.request.moneyLayers; const m4Journal = (0, settlement_core_1.resolveM4OutboundJournal)(layers, { glM1: profile.settlement.glM1 ?? settlement_core_1.GL_CODES.M1_CIRCULATING, glM2: profile.settlement.glM2 ?? settlement_core_1.GL_CODES.M2_BROAD, glM4NearMoney: profile.settlement.glM4NearMoney ?? profile.settlement.glSettlement ?? settlement_core_1.GL_CODES.SETTLEMENT_SUSPENSE, }); const debitGl = m4Journal?.debitGl ?? profile.settlement.glM2 ?? settlement_core_1.GL_CODES.M2_BROAD; const creditGl = m4Journal?.creditGl ?? (profile.settlement.glSettlement ?? settlement_core_1.GL_CODES.SETTLEMENT_SUSPENSE); const [debitGlId, creditGlId] = await Promise.all([ (0, fineract_1.resolveGlAccountId)(debitGl), (0, fineract_1.resolveGlAccountId)(creditGl), ]); const je = await (0, fineract_1.postJournalEntry)({ officeId, transactionDate: now.slice(0, 10), referenceNumber: req.idempotencyKey, comments: `${req.rail} transfer ${req.tokenSymbol}`, debits: [{ glAccountId: debitGlId, amount }], credits: [{ glAccountId: creditGlId, amount }], }); advance('FINERACT_POSTED', { fineractJournalRef: String(je.resourceId) }); } else if (amount > 0 && !isCryptoExternal) { const debitGl = profile.settlement.glM2 ?? settlement_core_1.GL_CODES.M2_BROAD; const creditGl = profile.settlement.glM3 ?? settlement_core_1.GL_CODES.M3_TOKEN_LIABILITY; const [debitGlId, creditGlId] = await Promise.all([ (0, fineract_1.resolveGlAccountId)(debitGl), (0, fineract_1.resolveGlAccountId)(creditGl), ]); const je = await (0, fineract_1.postJournalEntry)({ officeId, transactionDate: now.slice(0, 10), referenceNumber: req.idempotencyKey, comments: `${req.rail} transfer ${req.tokenSymbol}`, debits: [{ glAccountId: debitGlId, amount }], credits: [{ glAccountId: creditGlId, amount }], }); advance('FINERACT_POSTED', { fineractJournalRef: String(je.resourceId) }); } else { // Crypto external: liability already on M3; on-chain move is the egress — skip double M2→M3 advance('FINERACT_POSTED'); } if (isBankExternal && cfg.rails.hybx.enabled) { const hybx = new hybx_production_1.HybxProductionRail(); const pay = await hybx.dispatchPayment({ idempotencyKey: req.idempotencyKey, amount: req.amount, currency: req.currency ?? 'USD', creditorIban: req.creditorIban, beneficiaryName: req.beneficiaryName ?? req.recipientAddress, remittanceInfo: req.remittanceInfo ?? `OMNL ${req.tokenSymbol} external transfer`, rail: req.rail === 'RTGS' ? 'RTGS' : 'SWIFT', }); advance('HYBX_RAIL_DISPATCHED', { hybxPaymentId: pay.paymentId }); } else { advance('HYBX_RAIL_DISPATCHED'); } if (isBankExternal && cfg.rails.swift.enabled && req.creditorIban) { const swiftRaw = (0, swift_1.buildMt103Stub)({ senderRef: req.idempotencyKey, currency: req.currency ?? 'USD', amount: req.amount, valueDate: now.slice(0, 10), orderingCustomer: cfg.omnlLegalName, beneficiary: req.beneficiaryName ?? req.recipientAddress, iban: req.creditorIban, remittance: req.remittanceInfo ?? `OMNL ${req.tokenSymbol}`, }); await (0, swift_1.forwardSwiftToListener)(swiftRaw); const iso = await (0, omnl_1.archiveIso20022)({ messageType: 'pacs.008', direction: 'OUTBOUND', raw: swiftRaw, settlementRef: record.settlementId, }); advance('ISO20022_ARCHIVED', { iso20022MessageId: iso.messageId }); } else { advance('ISO20022_ARCHIVED'); } const doChain = (req.rail === 'INTERNAL' || req.rail === 'CHAIN138' || isCryptoExternal) && Boolean(req.tokenAddress) && (0, btc_l1_settlement_1.allowChainMintExecute)(); if (doChain) { const xfer = await (0, omnl_1.requestTokenTransfer)({ tokenAddress: req.tokenAddress, amount: req.amount, recipient: req.recipientAddress, settlementRef: record.settlementId, dryRun: false, symbol: req.tokenSymbol, }); advance('CHAIN_MINT_REQUESTED', { chainTxHash: xfer.txHash, verbiageDocument: [ record.verbiageDocument, isCryptoExternal ? `EXTERNAL on-chain ERC-20 egress: ${req.tokenAddress} → ${req.recipientAddress}` : `On-chain ERC-20 transfer: ${req.tokenAddress} → ${req.recipientAddress}`, xfer.txHash ? `tx ${xfer.txHash}` : `status ${xfer.status}`, ].join('\n'), }); } else { advance('CHAIN_MINT_REQUESTED', { verbiageDocument: [ record.verbiageDocument, isBankExternal ? 'Bank rail (SWIFT/IBAN) — no on-chain transfer' : `On-chain ERC-20 transfer prepared (execute flag off): ${req.tokenAddress} → ${req.recipientAddress}`, ].join('\n'), }); } advance('SETTLED'); return record; } catch (err) { record.errors.push(err instanceof Error ? err.message : String(err)); advance('FAILED'); return record; } }