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
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:
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Unit tests for BTC L1 settlement helpers (pricing gate + amount math).
|
||||
* Run via: npm run build && npm test (node --test)
|
||||
*/
|
||||
import { describe, it, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
btcUsdToJournalAmount,
|
||||
satsToBtcDecimal,
|
||||
} from '@dbis/settlement-core';
|
||||
|
||||
describe('btc-l1-settlement amounts', () => {
|
||||
it('mints cBTC amount from sats at 8 decimals', () => {
|
||||
assert.equal(satsToBtcDecimal(25_000_000), '0.25000000');
|
||||
});
|
||||
|
||||
it('posts USD fair value = sats × rate', () => {
|
||||
// 0.5 BTC @ 92_500 = 46_250.00
|
||||
assert.equal(btcUsdToJournalAmount(50_000_000, 92_500), '46250.00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('pricing gate reject static fallback', () => {
|
||||
it('flags 90000 as suspicious when live required', () => {
|
||||
const rate = 90000;
|
||||
const requireLive = true;
|
||||
const envOverride = Number('');
|
||||
const isSuspicious =
|
||||
requireLive &&
|
||||
Math.abs(rate - 90000) < 0.01 &&
|
||||
!(envOverride > 0 && Math.abs(envOverride - 90000) < 0.01);
|
||||
assert.equal(isSuspicious, true);
|
||||
});
|
||||
|
||||
it('allows non-fallback live rates', () => {
|
||||
const rate = 97_432.55;
|
||||
const requireLive = true;
|
||||
const isSuspicious = requireLive && Math.abs(rate - 90000) < 0.01;
|
||||
assert.equal(isSuspicious, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('idempotency key shape', () => {
|
||||
it('derives stable key from txId + instructionId', () => {
|
||||
const txId = 'deadbeef';
|
||||
const instructionId = 'inst-42';
|
||||
const key = `btc-l1-${txId}-${instructionId}`;
|
||||
assert.equal(key, 'btc-l1-deadbeef-inst-42');
|
||||
});
|
||||
});
|
||||
|
||||
describe('allowChainMintExecute env override', () => {
|
||||
it('treats SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=0 as dry-run', () => {
|
||||
const env: string = '0';
|
||||
const cfgAllow = true;
|
||||
const allow =
|
||||
env === '0' || env === 'false' ? false : env === '1' || env === 'true' ? true : cfgAllow;
|
||||
assert.equal(allow, false);
|
||||
});
|
||||
|
||||
it('treats SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=1 as execute', () => {
|
||||
const env: string = '1';
|
||||
const cfgAllow = false;
|
||||
const allow =
|
||||
env === '0' || env === 'false' ? false : env === '1' || env === 'true' ? true : cfgAllow;
|
||||
assert.equal(allow, true);
|
||||
});
|
||||
});
|
||||
|
||||
// silence unused mock import in older node
|
||||
void mock;
|
||||
@@ -0,0 +1,317 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
buildMoneySupplySnapshot,
|
||||
btcUsdToJournalAmount,
|
||||
GL_CODES,
|
||||
m2FiatToTokenLoadAmount,
|
||||
m3TokenLoadAmount,
|
||||
resolveBtcL1CustodyJournal,
|
||||
satsToBtcDecimal,
|
||||
type BtcL1ReconcileRequest,
|
||||
type BtcL1SettlementRequest,
|
||||
type SettlementRecord,
|
||||
} from '@dbis/settlement-core';
|
||||
import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config';
|
||||
import {
|
||||
fetchGlBalances,
|
||||
postJournalEntry,
|
||||
resolveBtcGlAccountIds,
|
||||
resolveGlAccountId,
|
||||
} from '../adapters/fineract';
|
||||
import { requestTokenMint } from '../adapters/omnl';
|
||||
import { resolveLiveBtcUsd } from '../adapters/pricing';
|
||||
import { settlementStore } from '../store/settlement-store';
|
||||
|
||||
const MIN_CONFIRMATIONS = Number(process.env.BTC_CONFIRMATIONS_REQUIRED || 6);
|
||||
|
||||
/** Env SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE overrides production JSON when set. */
|
||||
export function allowChainMintExecute(): boolean {
|
||||
const env = process.env.SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE?.trim();
|
||||
if (env === '0' || env === 'false') return false;
|
||||
if (env === '1' || env === 'true') return true;
|
||||
return loadConfig().production.allowChainMintExecute === true;
|
||||
}
|
||||
|
||||
export function formatSettlementError(err: unknown): string {
|
||||
if (axios.isAxiosError(err)) {
|
||||
const status = err.response?.status;
|
||||
const body = err.response?.data;
|
||||
const detail =
|
||||
typeof body === 'string'
|
||||
? body.slice(0, 500)
|
||||
: body != null
|
||||
? JSON.stringify(body).slice(0, 500)
|
||||
: err.message;
|
||||
return status != null ? `HTTP ${status}: ${detail}` : detail;
|
||||
}
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
|
||||
/**
|
||||
* L1 BTC deposit confirmed → Fineract custody journal (12015/12424) +
|
||||
* M2→M3 cBTC mint on Chain 138, valued at live BTC/USD.
|
||||
*/
|
||||
export async function processBtcL1Settlement(
|
||||
input: BtcL1SettlementRequest,
|
||||
): Promise<SettlementRecord> {
|
||||
const profile = loadOfficeProfile();
|
||||
const officeId = settlementOfficeId(input.officeId);
|
||||
const req = { ...input, officeId };
|
||||
|
||||
if (!req.idempotencyKey || !req.txId || !req.instructionId) {
|
||||
throw new Error('idempotencyKey, txId, and instructionId are required');
|
||||
}
|
||||
if (!(req.amountSats > 0)) {
|
||||
throw new Error('amountSats must be positive');
|
||||
}
|
||||
if (!req.recipientAddress?.startsWith('0x')) {
|
||||
throw new Error('recipientAddress (0x Chain 138 vault) required');
|
||||
}
|
||||
if ((req.confirmations ?? 0) < MIN_CONFIRMATIONS) {
|
||||
throw new Error(
|
||||
`Insufficient confirmations: ${req.confirmations ?? 0} < ${MIN_CONFIRMATIONS}`,
|
||||
);
|
||||
}
|
||||
|
||||
const existing = settlementStore.getByIdempotencyKey(req.idempotencyKey);
|
||||
if (existing?.phase === 'SETTLED') return existing;
|
||||
|
||||
// Fail-closed: FAILED BTC settles must be reconciled, not blindly retried
|
||||
// (avoids double journals / double mint after partial or out-of-band completion).
|
||||
if (existing?.phase === 'FAILED') {
|
||||
throw new Error(
|
||||
`BTC settlement ${existing.settlementId} is FAILED — use POST /btc/l1-reconcile ` +
|
||||
`(idempotencyKey=${existing.idempotencyKey}) instead of re-settling`,
|
||||
);
|
||||
}
|
||||
|
||||
// Also idempotent on txId — avoid double-posting same L1 deposit
|
||||
const byTx = settlementStore
|
||||
.list(500)
|
||||
.find((r) => r.btcSettlement?.txId === req.txId && r.phase === 'SETTLED');
|
||||
if (byTx) return byTx;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const valueDate = req.valueDate ?? now.slice(0, 10);
|
||||
const btcAmount = satsToBtcDecimal(req.amountSats);
|
||||
|
||||
let record: SettlementRecord = existing ?? {
|
||||
settlementId: uuidv4(),
|
||||
idempotencyKey: req.idempotencyKey,
|
||||
phase: 'RECEIVED',
|
||||
request: {
|
||||
idempotencyKey: req.idempotencyKey,
|
||||
officeId,
|
||||
valueDate,
|
||||
currency: 'BTC',
|
||||
amount: btcAmount,
|
||||
creditorIban: 'INTERNAL-BTC-L1-CUSTODY',
|
||||
beneficiaryName: req.recipientAddress,
|
||||
remittanceInfo: `L1 BTC settle tx=${req.txId} instruction=${req.instructionId} → cBTC`,
|
||||
moneyLayers: ['M0', 'M2', 'M3'],
|
||||
rail: 'CHAIN138',
|
||||
convertToCrypto: {
|
||||
enabled: true,
|
||||
targetChainId: 138,
|
||||
tokenLineId: 'BTC-M2',
|
||||
recipientAddress: req.recipientAddress,
|
||||
},
|
||||
},
|
||||
errors: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const advance = (phase: SettlementRecord['phase'], patch: Partial<SettlementRecord> = {}) => {
|
||||
record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch };
|
||||
settlementStore.save(record);
|
||||
};
|
||||
|
||||
try {
|
||||
advance('VALIDATED');
|
||||
|
||||
const quote = await resolveLiveBtcUsd();
|
||||
const usdValue = btcUsdToJournalAmount(req.amountSats, quote.btcUsdRate);
|
||||
const usdAmount = parseFloat(usdValue) || 0;
|
||||
if (!(usdAmount > 0)) {
|
||||
throw new Error(`USD journal amount invalid for ${req.amountSats} sats @ ${quote.btcUsdRate}`);
|
||||
}
|
||||
|
||||
const custody = resolveBtcL1CustodyJournal(officeId);
|
||||
advance('VERBIAGE_ROLLED', {
|
||||
btcSettlement: {
|
||||
amountSats: req.amountSats,
|
||||
txId: req.txId,
|
||||
btcUsdRate: quote.btcUsdRate,
|
||||
usdValue,
|
||||
depositAddress: req.depositAddress,
|
||||
instructionId: req.instructionId,
|
||||
priceSource: quote.source,
|
||||
priceAsOf: quote.asOf,
|
||||
},
|
||||
verbiageDocument: [
|
||||
`OMNL L1 BTC custody settlement (IFRS 13 fair value)`,
|
||||
`Office ${officeId} · instruction ${req.instructionId}`,
|
||||
`L1 tx ${req.txId} · ${req.confirmations} confirmations`,
|
||||
`Deposit ${req.depositAddress}`,
|
||||
`Amount ${btcAmount} BTC (${req.amountSats} sats)`,
|
||||
`BTC/USD ${quote.btcUsdRate} (${quote.source} @ ${quote.asOf}) → USD ${usdValue}`,
|
||||
`Custody journal ${custody.memo}: Dr ${custody.debitGl} / Cr ${custody.creditGl}`,
|
||||
`M2→M3 cBTC mint BTC-M2 → ${req.recipientAddress}`,
|
||||
`Note: cBTC real-liquidity activation maps to WETH/USDT/USDC on Chain 138, not L1 BTC payout`,
|
||||
].join('\n'),
|
||||
});
|
||||
|
||||
// 1) L1 custody journal — HO office 1 posts Dr 12015 / Cr 12424
|
||||
const btcGl = await resolveBtcGlAccountIds(officeId);
|
||||
const custodyJe = await postJournalEntry({
|
||||
officeId: 1,
|
||||
transactionDate: valueDate,
|
||||
referenceNumber: `${req.idempotencyKey}-L1`,
|
||||
comments: `${custody.memo} ${req.txId} ${usdValue} USD`,
|
||||
debits: [{ glAccountId: btcGl.custodyHoGlId, amount: usdAmount }],
|
||||
credits: [{ glAccountId: btcGl.interofficeBtcGlId, amount: usdAmount }],
|
||||
});
|
||||
|
||||
// 2) M2→M3 backing check + journal at Office 24 (USD fair value)
|
||||
const balances = await fetchGlBalances(officeId);
|
||||
const snap = buildMoneySupplySnapshot(officeId, balances);
|
||||
const { loadAmount, fullyBacked } = m2FiatToTokenLoadAmount(usdValue, snap.M2.broadMoney);
|
||||
const m3Check = m3TokenLoadAmount(loadAmount, snap.M2.broadMoney, snap.M3.tokenLiabilities);
|
||||
if (!fullyBacked || !m3Check.fullyBacked) {
|
||||
record.errors.push(
|
||||
`M2/M3 backing insufficient for BTC settlement (M2 load ${loadAmount}/${usdValue}, M3 headroom ${m3Check.loadAmount})`,
|
||||
);
|
||||
advance('FAILED', {
|
||||
fineractJournalRef: String(custodyJe.resourceId),
|
||||
});
|
||||
return record;
|
||||
}
|
||||
|
||||
const m2Amount = parseFloat(loadAmount) || 0;
|
||||
let m2m3Ref = '';
|
||||
if (m2Amount > 0) {
|
||||
const [debitGlId, creditGlId] = await Promise.all([
|
||||
resolveGlAccountId(profile.settlement.glM2 ?? GL_CODES.M2_BROAD),
|
||||
resolveGlAccountId(profile.settlement.glM3 ?? GL_CODES.M3_TOKEN_LIABILITY),
|
||||
]);
|
||||
const m2Je = await postJournalEntry({
|
||||
officeId,
|
||||
transactionDate: valueDate,
|
||||
referenceNumber: `${req.idempotencyKey}-M2M3`,
|
||||
comments: `T-BTC-M2M3 cBTC load ${btcAmount} BTC`,
|
||||
debits: [{ glAccountId: debitGlId, amount: m2Amount }],
|
||||
credits: [{ glAccountId: creditGlId, amount: m2Amount }],
|
||||
});
|
||||
m2m3Ref = String(m2Je.resourceId);
|
||||
}
|
||||
|
||||
advance('FINERACT_POSTED', {
|
||||
fineractJournalRef: [String(custodyJe.resourceId), m2m3Ref].filter(Boolean).join(','),
|
||||
});
|
||||
advance('HYBX_RAIL_DISPATCHED');
|
||||
advance('ISO20022_ARCHIVED');
|
||||
|
||||
// 3) Mint cBTC in satoshi-accurate BTC decimal amount
|
||||
const mint = await requestTokenMint({
|
||||
lineId: 'BTC-M2',
|
||||
amount: btcAmount,
|
||||
recipient: req.recipientAddress,
|
||||
settlementRef: record.settlementId,
|
||||
dryRun: !allowChainMintExecute(),
|
||||
symbol: 'cBTC',
|
||||
});
|
||||
advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash });
|
||||
advance('SETTLED');
|
||||
return record;
|
||||
} catch (err) {
|
||||
record.errors.push(formatSettlementError(err));
|
||||
advance('FAILED');
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force SETTLED when custody journals + cBTC mint already completed out-of-band.
|
||||
* Does not call Fineract or mint again.
|
||||
*/
|
||||
export function reconcileBtcL1Settlement(input: BtcL1ReconcileRequest): SettlementRecord {
|
||||
if (!input.settlementId && !input.idempotencyKey) {
|
||||
throw new Error('settlementId or idempotencyKey required');
|
||||
}
|
||||
if (!(input.amountSats > 0)) {
|
||||
throw new Error('amountSats must be positive');
|
||||
}
|
||||
if (!input.chainTxHash?.startsWith('0x')) {
|
||||
throw new Error('chainTxHash (0x…) required');
|
||||
}
|
||||
if (!(input.btcUsdRate > 0)) {
|
||||
throw new Error('btcUsdRate must be positive');
|
||||
}
|
||||
if (!input.recipientAddress?.startsWith('0x')) {
|
||||
throw new Error('recipientAddress required');
|
||||
}
|
||||
|
||||
const existing = input.settlementId
|
||||
? settlementStore.getById(input.settlementId)
|
||||
: settlementStore.getByIdempotencyKey(input.idempotencyKey!);
|
||||
|
||||
if (!existing) {
|
||||
throw new Error(
|
||||
`Settlement not found (id=${input.settlementId ?? ''} key=${input.idempotencyKey ?? ''})`,
|
||||
);
|
||||
}
|
||||
if (existing.phase === 'SETTLED' && existing.btcSettlement?.amountSats === input.amountSats) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const btcAmount = satsToBtcDecimal(input.amountSats);
|
||||
const usdValue =
|
||||
input.usdValue ?? btcUsdToJournalAmount(input.amountSats, input.btcUsdRate);
|
||||
const officeId = settlementOfficeId(existing.request.officeId);
|
||||
|
||||
const record: SettlementRecord = {
|
||||
...existing,
|
||||
phase: 'SETTLED',
|
||||
updatedAt: now,
|
||||
chainTxHash: input.chainTxHash,
|
||||
fineractJournalRef: input.fineractJournalRef ?? existing.fineractJournalRef,
|
||||
errors: [],
|
||||
btcSettlement: {
|
||||
amountSats: input.amountSats,
|
||||
txId: input.txId,
|
||||
btcUsdRate: input.btcUsdRate,
|
||||
usdValue,
|
||||
depositAddress: input.depositAddress,
|
||||
instructionId: input.instructionId,
|
||||
priceSource: input.priceSource ?? 'reconcile/ops',
|
||||
priceAsOf: input.priceAsOf ?? now,
|
||||
},
|
||||
verbiageDocument: [
|
||||
`OMNL L1 BTC custody settlement — RECONCILED (no remint / no re-journal)`,
|
||||
`Office ${officeId} · instruction ${input.instructionId}`,
|
||||
`L1/ops tx ${input.txId}`,
|
||||
`Amount ${btcAmount} BTC (${input.amountSats} sats)`,
|
||||
`BTC/USD ${input.btcUsdRate} → USD ${usdValue}`,
|
||||
`Chain mint ${input.chainTxHash} → ${input.recipientAddress}`,
|
||||
input.fineractJournalRef ? `Fineract refs ${input.fineractJournalRef}` : 'Fineract refs: prior out-of-band journals',
|
||||
].join('\n'),
|
||||
request: {
|
||||
...existing.request,
|
||||
amount: btcAmount,
|
||||
beneficiaryName: input.recipientAddress,
|
||||
remittanceInfo: `RECONCILED L1 BTC settle tx=${input.txId} → cBTC ${input.chainTxHash}`,
|
||||
convertToCrypto: {
|
||||
enabled: true,
|
||||
targetChainId: 138,
|
||||
tokenLineId: 'BTC-M2',
|
||||
recipientAddress: input.recipientAddress,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
settlementStore.save(record);
|
||||
return record;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { FiatLpSwapLeg, FiatLpToLiquidityRequest, SettlementRecord } from '@dbis/settlement-core';
|
||||
import { loadConfig, settlementOfficeId } from '../config';
|
||||
import { requestFiatLpToLiquidity } from '../adapters/omnl';
|
||||
import { settlementStore } from '../store/settlement-store';
|
||||
|
||||
export async function processFiatLpToLiquidity(
|
||||
input: FiatLpToLiquidityRequest,
|
||||
): Promise<SettlementRecord> {
|
||||
const cfg = loadConfig();
|
||||
const officeId = settlementOfficeId(input.officeId);
|
||||
const req = { ...input, officeId };
|
||||
const holder = req.holderAddress.trim();
|
||||
const recipient = (req.recipientAddress ?? holder).trim();
|
||||
|
||||
const existing = settlementStore.getByIdempotencyKey(req.idempotencyKey);
|
||||
if (existing?.phase === 'SETTLED') return existing;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
let record: SettlementRecord = existing ?? {
|
||||
settlementId: uuidv4(),
|
||||
idempotencyKey: req.idempotencyKey,
|
||||
phase: 'RECEIVED',
|
||||
request: {
|
||||
idempotencyKey: req.idempotencyKey,
|
||||
officeId,
|
||||
valueDate: now.slice(0, 10),
|
||||
currency: 'USD',
|
||||
amount: req.amount ?? '0',
|
||||
creditorIban: 'INTERNAL-FIAT-LP-LIQUIDITY',
|
||||
beneficiaryName: recipient,
|
||||
remittanceInfo: `Chain 138 fiat LP → real liquidity (${req.convertAll ? 'all c*' : req.symbol ?? 'single'})`,
|
||||
moneyLayers: ['M2', 'M3'],
|
||||
rail: 'CHAIN138',
|
||||
},
|
||||
errors: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const advance = (phase: SettlementRecord['phase'], patch: Partial<SettlementRecord> = {}) => {
|
||||
record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch };
|
||||
settlementStore.save(record);
|
||||
};
|
||||
|
||||
try {
|
||||
advance('VALIDATED');
|
||||
advance('VERBIAGE_ROLLED', {
|
||||
verbiageDocument: [
|
||||
'OMNL Chain 138 fiat LP → real fund liquidity activation',
|
||||
`Holder ${holder} → recipient ${recipient}`,
|
||||
req.convertAll ? 'Convert all swappable c* tokens' : `Symbol ${req.symbol ?? 'n/a'}`,
|
||||
'Target: official USDT / USDC / WETH on Chain 138 (mainnet-style parity)',
|
||||
].join('\n'),
|
||||
});
|
||||
advance('FINERACT_POSTED');
|
||||
advance('HYBX_RAIL_DISPATCHED');
|
||||
advance('ISO20022_ARCHIVED');
|
||||
|
||||
const dryRun = req.dryRun ?? !cfg.production.allowChainMintExecute;
|
||||
const result = await requestFiatLpToLiquidity({
|
||||
holderAddress: holder,
|
||||
recipientAddress: recipient,
|
||||
symbol: req.symbol,
|
||||
amount: req.amount,
|
||||
convertAll: req.convertAll,
|
||||
maxSlippageBps: req.maxSlippageBps,
|
||||
settlementRef: record.settlementId,
|
||||
dryRun,
|
||||
});
|
||||
|
||||
const liquiditySwaps: FiatLpSwapLeg[] = result.swaps.map((s) => ({
|
||||
lpSymbol: s.lpSymbol,
|
||||
tokenIn: s.tokenIn,
|
||||
tokenOut: s.tokenOut,
|
||||
realSymbol: s.realSymbol,
|
||||
amountIn: s.amountIn,
|
||||
amountOutQuoted: s.amountOutQuoted,
|
||||
txHash: s.txHash,
|
||||
status: s.status as FiatLpSwapLeg['status'],
|
||||
error: s.error,
|
||||
}));
|
||||
|
||||
const lastTx = liquiditySwaps.find((s) => s.txHash)?.txHash;
|
||||
const anyFailed = liquiditySwaps.some((s) => s.status === 'FAILED');
|
||||
|
||||
advance('CHAIN_MINT_REQUESTED', {
|
||||
chainTxHash: lastTx,
|
||||
liquiditySwaps,
|
||||
});
|
||||
|
||||
if (anyFailed && !dryRun) {
|
||||
record.errors.push('One or more LP→liquidity swaps failed');
|
||||
advance('FAILED');
|
||||
return record;
|
||||
}
|
||||
|
||||
advance('SETTLED');
|
||||
return record;
|
||||
} catch (err) {
|
||||
record.errors.push(err instanceof Error ? err.message : String(err));
|
||||
advance('FAILED');
|
||||
return record;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user