diff --git a/.gitignore b/.gitignore index 4d50296..cc507cf 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ secrets/ besu-data/ chaindata/ datadir/ +data/bitcoind-regtest/ +data/btc-intake/ +data/settlement-store/ # Foundry out/ @@ -68,6 +71,11 @@ node_modules/ # TypeScript service emit (build in-tree) services/btc-intake/dist/ +# Local settlement store / runtime data (do not commit) +services/data/ +logs/ +dist/go-live/ + # Python __pycache__/ *.pyc diff --git a/config/bitcoind.regtest.conf b/config/bitcoind.regtest.conf new file mode 100644 index 0000000..9102c38 --- /dev/null +++ b/config/bitcoind.regtest.conf @@ -0,0 +1,10 @@ +regtest=1 +server=1 +txindex=1 +rpcuser=omnl +rpcpassword=omnl-regtest-local +rpcallowip=0.0.0.0/0 +rpcbind=0.0.0.0 +rpcport=18443 +fallbackfee=0.0002 +printtoconsole=1 diff --git a/config/chain138-fiat-lp-liquidity.v1.json b/config/chain138-fiat-lp-liquidity.v1.json new file mode 100644 index 0000000..2671eff --- /dev/null +++ b/config/chain138-fiat-lp-liquidity.v1.json @@ -0,0 +1,76 @@ +{ + "$schema": "Chain 138 fiat LP (c*) → real on-chain liquidity — mainnet-style parity", + "version": "1.0.0", + "updated": "2026-07-06", + "chainId": 138, + "mainnetParityChainId": 1, + "note": "Real fund tokens use canonical Chain 138 addresses (same symbols as Ethereum mainnet family). LP c* tokens swap via EnhancedSwapRouterV2 / DODO PMM.", + "contracts": { + "enhancedSwapRouterV2": "0xa421706768aeb7fafa2d912c5e10824ef3437ad4", + "enhancedSwapRouterV1": "0xE6Cc7643ae2A4C720A28D8263BC4972905d7DE0f", + "uniswapV3Router": "0xde9cD8ee2811E6E64a41D5F68Be315d33995975E", + "dodoPmmIntegration": "0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895", + "uniswapV3WethUsdtPool": "0xa893add35aEfe6A6d858EB01828bE4592f12C9F5", + "uniswapV3WethUsdcPool": "0xEC745bfb6b3cd32f102d594E5F432d8d85B19391", + "officialUsdt": "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1", + "officialUsdc": "0x71D6687F38b93CCad569Fa6352c876eea967201b", + "officialWeth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "ccipWeth9Bridge": "0xcacfd227A040002e49e2e01626363071324f820a" + }, + "mainnetReference": { + "note": "Chain 1 canonical references for parity documentation", + "usdt": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + }, + "defaultSlippageBps": 50, + "minSwapAmountRaw": "1000", + "lpPrefix": "c", + "realTargets": { + "USD": "USDT", + "EUR": "USDT", + "GBP": "USDT", + "AUD": "USDT", + "JPY": "USDT", + "CHF": "USDT", + "CAD": "USDT", + "XAU": "WETH", + "BTC": "WETH", + "ETH": "WETH", + "BNB": "WETH", + "POL": "WETH", + "AVAX": "WETH", + "CRO": "WETH", + "XDAI": "WETH", + "CELO": "WETH", + "WEMIX": "WETH" + }, + "explicitLpToReal": [ + { "lpSymbol": "cUSDT", "realSymbol": "USDT" }, + { "lpSymbol": "cUSDT_V2", "realSymbol": "USDT" }, + { "lpSymbol": "cAUSDT", "realSymbol": "USDT" }, + { "lpSymbol": "cUSDW", "realSymbol": "USDT" }, + { "lpSymbol": "cUSDC", "realSymbol": "USDC" }, + { "lpSymbol": "cUSDC_V2", "realSymbol": "USDC" }, + { "lpSymbol": "cEURC", "realSymbol": "USDT" }, + { "lpSymbol": "cEURT", "realSymbol": "USDT" }, + { "lpSymbol": "cGBPC", "realSymbol": "USDT" }, + { "lpSymbol": "cGBPT", "realSymbol": "USDT" }, + { "lpSymbol": "cAUDC", "realSymbol": "USDT" }, + { "lpSymbol": "cJPYC", "realSymbol": "USDT" }, + { "lpSymbol": "cCHFC", "realSymbol": "USDT" }, + { "lpSymbol": "cCADC", "realSymbol": "USDT" }, + { "lpSymbol": "cXAUC", "realSymbol": "WETH" }, + { "lpSymbol": "cXAUT", "realSymbol": "WETH" }, + { "lpSymbol": "cBTC", "realSymbol": "WETH" }, + { "lpSymbol": "cETH", "realSymbol": "WETH" }, + { "lpSymbol": "cETHL2", "realSymbol": "WETH" }, + { "lpSymbol": "cBNB", "realSymbol": "WETH" }, + { "lpSymbol": "cPOL", "realSymbol": "WETH" }, + { "lpSymbol": "cAVAX", "realSymbol": "WETH" }, + { "lpSymbol": "cCRO", "realSymbol": "WETH" }, + { "lpSymbol": "cXDAI", "realSymbol": "WETH" }, + { "lpSymbol": "cCELO", "realSymbol": "WETH" }, + { "lpSymbol": "cWEMIX", "realSymbol": "WETH" } + ] +} diff --git a/config/custody-dfns-cobo.env.example b/config/custody-dfns-cobo.env.example new file mode 100644 index 0000000..e9f9f23 --- /dev/null +++ b/config/custody-dfns-cobo.env.example @@ -0,0 +1,13 @@ +# Dfns + Cobo custody credentials (copy from secrets/* after local setup) +# Real secrets live only under secrets/ (gitignored). Do not put live tokens here. + +DFNS_API_BASE_URL=https://api.dfns.io +DFNS_ORG_ID= +DFNS_AUTH_TOKEN= +DFNS_CRED_ID= +DFNS_PRIVATE_KEY_PATH=secrets/dfns/rsa2048.pem + +COBO_API_BASE_URL=https://api.cobo.com/v2 +COBO_API_KEY= +COBO_API_SECRET= +# Temporary keys: expires noted by issuer. Dev portal keys use https://api.dev.cobo.com/v2 diff --git a/config/deployment-omnl.production.env.example b/config/deployment-omnl.production.env.example index ece7ef2..bb72ccb 100644 --- a/config/deployment-omnl.production.env.example +++ b/config/deployment-omnl.production.env.example @@ -74,3 +74,29 @@ VITE_WALLETCONNECT_PROJECT_ID= VITE_THIRDWEB_CLIENT_ID= VITE_RPC_URL_138=http://192.168.11.221:8545 VITE_ENHANCED_SWAP_ROUTER_V2=0xa421706768aeb7fafa2d912c5e10824ef3437ad4 + +# === BTC L1 custody + live pricing === +OMNL_REQUIRE_LIVE_BTC_PRICE=1 +ISO4217_FX_REFRESH_INTERVAL_MS=60000 +# Optional override if oracle down (only set deliberately): +# CHAIN138_CANONICAL_PRICE_USD_BTC= +BITCOIND_RPC_URL= +BITCOIND_RPC_USER= +BITCOIND_RPC_PASS= +BTC_NETWORK=mainnet +BTC_RESERVE_ADDRESSES= +BTC_CONFIRMATIONS_REQUIRED=6 +BTC_INTAKE_PORT=3013 +BTC_INTAKE_SYNC_INTERVAL_MS=30000 +BTC_MAX_OUTSTANDING_SATS= +SETTLEMENT_MIDDLEWARE_URL=http://127.0.0.1:3011 +# cBTC PMM pools (Chain 138 DODO) — set after deploy +CHAIN138_POOL_CBTC_CUSDT= +CHAIN138_POOL_CBTC_CUSDC= +CHAIN138_POOL_CBTC_CXAUC= + +# After rebuilding settlement-middleware, redeploy so secure.omdnl.org/settlement/btc/* is live. +# Smoke: SETTLEMENT_URL=https://secure.omdnl.org/settlement OMNL_API_KEY=... node scripts/deployment/smoke-btc-settlement.mjs +# Hot LP: HOT_CBTC_EXECUTE=1 node scripts/deployment/hot-cbtc-liquidity-1000.mjs +# Trade: node scripts/deployment/verify-cbtc-trade.mjs + diff --git a/config/omnl-ipsas-gl-registry.json b/config/omnl-ipsas-gl-registry.json index b06c64e..683f744 100644 --- a/config/omnl-ipsas-gl-registry.json +++ b/config/omnl-ipsas-gl-registry.json @@ -93,7 +93,7 @@ "ipsasStandards": ["IPSAS 2", "IPSAS 28", "IPSAS 29"], "ifrsRefs": ["IAS 21", "IFRS 13"], "roles": ["digital_asset_reserve", "btc_l1_custody"], - "inLiveRegistry": false + "inLiveRegistry": true }, { "glCode": "12424", @@ -104,7 +104,7 @@ "ifrsRefs": ["IFRS 13"], "roles": ["digital_asset_custody", "interoffice_btc"], "officeId": 24, - "inLiveRegistry": false + "inLiveRegistry": true }, { "glCode": "13010", diff --git a/config/omnl-journal-matrix.json b/config/omnl-journal-matrix.json index 6019c2d..c55b067 100644 --- a/config/omnl-journal-matrix.json +++ b/config/omnl-journal-matrix.json @@ -356,6 +356,26 @@ "ipsasRef": "IPSAS 28", "moneyLayer": "M1", "variableAmount": true + }, + { + "memo": "T-BTC-L1-HO", + "officeId": 1, + "debitGlCode": "12015", + "creditGlCode": "12424", + "amount": 0, + "narrative": "L1 BTC custody confirm — Dr HO digital asset reserve / Cr Office 24 interoffice BTC (USD = sats × live BTC/USD)", + "ipsasRef": "IPSAS 2, 28, 29; IFRS 13", + "moneyLayer": "M0" + }, + { + "memo": "T-BTC-M2M3", + "officeId": 24, + "debitGlCode": "2200", + "creditGlCode": "2300", + "amount": 0, + "narrative": "BTC-M2 cBTC liability load — Dr M2 broad money / Cr M3 token liability", + "ipsasRef": "IPSAS 28, 29", + "moneyLayer": "M3" } ] } diff --git a/docker-compose.bitcoind-regtest.yml b/docker-compose.bitcoind-regtest.yml new file mode 100644 index 0000000..b42359a --- /dev/null +++ b/docker-compose.bitcoind-regtest.yml @@ -0,0 +1,23 @@ +# Local regtest bitcoind for BTC intake E2E +# Usage: docker compose -f docker-compose.bitcoind-regtest.yml up -d + +services: + bitcoind-regtest: + image: bitcoin/bitcoin:27.0 + container_name: omnl-bitcoind-regtest + ports: + - "18443:18443" + volumes: + - ./data/bitcoind-regtest:/home/bitcoin/.bitcoin + command: + - -regtest=1 + - -server=1 + - -txindex=1 + - -rpcuser=omnl + - -rpcpassword=omnl-regtest-local + - -rpcallowip=0.0.0.0/0 + - -rpcbind=0.0.0.0 + - -rpcport=18443 + - -fallbackfee=0.0002 + - -printtoconsole=1 + restart: unless-stopped diff --git a/docker-compose.omnl-production.yml b/docker-compose.omnl-production.yml index 329c23e..a1df940 100644 --- a/docker-compose.omnl-production.yml +++ b/docker-compose.omnl-production.yml @@ -93,7 +93,62 @@ services: - token-aggregation - settlement-middleware - dbis-exchange + - btc-intake restart: unless-stopped + # Optional: use external BITCOIND_RPC_URL instead of this service + bitcoind: + image: bitcoin/bitcoin:27.0 + profiles: ["btc-node"] + volumes: + - btc-data:/home/bitcoin/.bitcoin + environment: + BITCOIN_DATA: /home/bitcoin/.bitcoin + command: + - bitcoind + - -server=1 + - -txindex=1 + - -rpcallowip=0.0.0.0/0 + - -rpcbind=0.0.0.0 + - -rpcuser=${BITCOIND_RPC_USER:-omnl} + - -rpcpassword=${BITCOIND_RPC_PASS:-changeme} + - -printtoconsole + expose: + - "8332" + restart: unless-stopped + + btc-intake: + build: + context: ./services/btc-intake + dockerfile: Dockerfile + ports: + - "3013:3013" + env_file: + - ./config/deployment-omnl.production.env + environment: + - NODE_ENV=production + - BTC_INTAKE_PORT=3013 + - BTC_INTAKE_DATA_DIR=/app/data + - SETTLEMENT_MIDDLEWARE_URL=http://settlement-middleware:3011 + - BITCOIND_RPC_URL=${BITCOIND_RPC_URL:-http://bitcoind:8332} + - BITCOIND_RPC_USER=${BITCOIND_RPC_USER:-omnl} + - BITCOIND_RPC_PASS=${BITCOIND_RPC_PASS:-changeme} + - BTC_NETWORK=${BTC_NETWORK:-mainnet} + - BTC_CONFIRMATIONS_REQUIRED=${BTC_CONFIRMATIONS_REQUIRED:-6} + - OMNL_REQUIRE_LIVE_BTC_PRICE=1 + volumes: + - btc-intake-data:/app/data + depends_on: + settlement-middleware: + condition: service_started + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3013/health',r=>process.exit(r.statusCode===200||r.statusCode===503?0:1))"] + interval: 30s + timeout: 5s + retries: 3 + volumes: settlement-data: + btc-data: + btc-intake-data: diff --git a/packages/settlement-core/dist/btc-settlement.test.d.ts b/packages/settlement-core/dist/btc-settlement.test.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/packages/settlement-core/dist/btc-settlement.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/settlement-core/dist/btc-settlement.test.js b/packages/settlement-core/dist/btc-settlement.test.js new file mode 100644 index 0000000..a159a25 --- /dev/null +++ b/packages/settlement-core/dist/btc-settlement.test.js @@ -0,0 +1,26 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const node_test_1 = require("node:test"); +const strict_1 = __importDefault(require("node:assert/strict")); +const money_supply_1 = require("./money-supply"); +(0, node_test_1.describe)('BTC settlement helpers', () => { + (0, node_test_1.it)('converts sats to 8-decimal BTC', () => { + strict_1.default.equal((0, money_supply_1.satsToBtcDecimal)(100_000_000), '1.00000000'); + strict_1.default.equal((0, money_supply_1.satsToBtcDecimal)(1), '0.00000001'); + strict_1.default.equal((0, money_supply_1.satsToBtcDecimal)(0), '0.00000000'); + }); + (0, node_test_1.it)('values sats at live BTC/USD for Fineract journal amount', () => { + strict_1.default.equal((0, money_supply_1.btcUsdToJournalAmount)(100_000_000, 95_000), '95000.00'); + strict_1.default.equal((0, money_supply_1.btcUsdToJournalAmount)(50_000_000, 100_000), '50000.00'); + strict_1.default.equal((0, money_supply_1.btcUsdToJournalAmount)(0, 100_000), '0.00'); + }); + (0, node_test_1.it)('resolves T-BTC-L1-HO custody journal pair', () => { + const j = (0, money_supply_1.resolveBtcL1CustodyJournal)(24); + strict_1.default.equal(j.debitGl, '12015'); + strict_1.default.equal(j.creditGl, '12424'); + strict_1.default.equal(j.memo, 'T-BTC-L1-HO'); + }); +}); diff --git a/packages/settlement-core/dist/money-supply.d.ts b/packages/settlement-core/dist/money-supply.d.ts index fa53b59..05a6de7 100644 --- a/packages/settlement-core/dist/money-supply.d.ts +++ b/packages/settlement-core/dist/money-supply.d.ts @@ -9,6 +9,8 @@ export declare const GL_CODES: { readonly SETTLEMENT_SUSPENSE: "1000"; readonly TRADE_FINANCE_CONTINGENT: "1410"; readonly CRYPTO_TOKEN_LIABILITY: "2300"; + readonly BTC_L1_CUSTODY_HO: "12015"; + readonly BTC_INTEROFFICE_O24: "12424"; }; /** Policy caps: M0→M1 (1.2×), M1→M2 (5×), M2→M3 token mint, M4 near-money rail */ export declare const POLICY: { @@ -61,3 +63,14 @@ export declare function resolveM4OutboundJournal(moneyLayers: MoneyLayer[], gl: debitGl: string; creditGl: string; } | null; +/** Convert satoshis to BTC decimal string (8 places). */ +export declare function satsToBtcDecimal(amountSats: number): string; +/** USD journal amount for Fineract = sats × BTC/USD rate (2 decimal places). */ +export declare function btcUsdToJournalAmount(amountSats: number, btcUsdRate: number): string; +/** IPSAS journal pair for L1 BTC custody confirm (T-BTC-L1-HO): Dr 12015 / Cr 12424. */ +export declare function resolveBtcL1CustodyJournal(_officeId?: number): { + debitGl: string; + creditGl: string; + narrative: string; + memo: string; +}; diff --git a/packages/settlement-core/dist/money-supply.js b/packages/settlement-core/dist/money-supply.js index 597c4e3..0bdc570 100644 --- a/packages/settlement-core/dist/money-supply.js +++ b/packages/settlement-core/dist/money-supply.js @@ -8,6 +8,9 @@ exports.m3TokenLoadAmount = m3TokenLoadAmount; exports.resolveZBankLayerJournal = resolveZBankLayerJournal; exports.zBankM0ToM4Hops = zBankM0ToM4Hops; exports.resolveM4OutboundJournal = resolveM4OutboundJournal; +exports.satsToBtcDecimal = satsToBtcDecimal; +exports.btcUsdToJournalAmount = btcUsdToJournalAmount; +exports.resolveBtcL1CustodyJournal = resolveBtcL1CustodyJournal; /** IPSAS GL mapping for OMNL central-bank money aggregates M0–M4 */ exports.GL_CODES = { M0_RESERVE: '1050', @@ -18,6 +21,8 @@ exports.GL_CODES = { SETTLEMENT_SUSPENSE: '1000', TRADE_FINANCE_CONTINGENT: '1410', CRYPTO_TOKEN_LIABILITY: '2300', + BTC_L1_CUSTODY_HO: '12015', + BTC_INTEROFFICE_O24: '12424', }; /** Policy caps: M0→M1 (1.2×), M1→M2 (5×), M2→M3 token mint, M4 near-money rail */ exports.POLICY = { @@ -165,3 +170,25 @@ function resolveM4OutboundJournal(moneyLayers, gl) { } return { debitGl: exports.GL_CODES.M2_BROAD, creditGl: suspense }; } +/** Convert satoshis to BTC decimal string (8 places). */ +function satsToBtcDecimal(amountSats) { + const sats = Math.max(0, Math.floor(Number(amountSats) || 0)); + return (sats / 1e8).toFixed(8); +} +/** USD journal amount for Fineract = sats × BTC/USD rate (2 decimal places). */ +function btcUsdToJournalAmount(amountSats, btcUsdRate) { + const sats = Math.max(0, Number(amountSats) || 0); + const rate = Number(btcUsdRate) || 0; + if (sats <= 0 || rate <= 0) + return '0.00'; + return ((sats / 1e8) * rate).toFixed(2); +} +/** IPSAS journal pair for L1 BTC custody confirm (T-BTC-L1-HO): Dr 12015 / Cr 12424. */ +function resolveBtcL1CustodyJournal(_officeId) { + return { + debitGl: exports.GL_CODES.BTC_L1_CUSTODY_HO, + creditGl: exports.GL_CODES.BTC_INTEROFFICE_O24, + narrative: 'L1 BTC custody confirm — Dr HO digital asset reserve / Cr Office 24 interoffice BTC', + memo: 'T-BTC-L1-HO', + }; +} diff --git a/packages/settlement-core/dist/types.d.ts b/packages/settlement-core/dist/types.d.ts index 5612fd7..f9559db 100644 --- a/packages/settlement-core/dist/types.d.ts +++ b/packages/settlement-core/dist/types.d.ts @@ -86,6 +86,79 @@ export type ZBankM0M4Request = { recipientAddress?: string; tokenLineId?: string; }; +/** Swap fiat-backed LP token (c*) → real fund on Chain 138 (USDT/USDC/WETH). */ +export type FiatLpToLiquidityRequest = { + idempotencyKey: string; + officeId?: number; + /** Wallet holding c* LP tokens (treasury or recipient after mint). */ + holderAddress: string; + /** Receiver of real USDT/USDC/WETH after swap; defaults to holderAddress. */ + recipientAddress?: string; + /** Single LP symbol (e.g. cUSDT); omit with convertAll for full portfolio. */ + symbol?: string; + amount?: string; + convertAll?: boolean; + maxSlippageBps?: number; + dryRun?: boolean; +}; +/** L1 Bitcoin deposit confirmed → Fineract custody journal + cBTC mint. */ +export type BtcL1SettlementRequest = { + idempotencyKey: string; + /** Sender office (default 24). */ + officeId?: number; + instructionId: string; + txId: string; + depositAddress: string; + amountSats: number; + confirmations: number; + /** Chain 138 vault for cBTC mint. */ + recipientAddress: string; + valueDate?: string; +}; +/** + * Mark a FAILED/partial BTC L1 settle as SETTLED when books + mint already + * happened out-of-band (no re-journal / no remint). + */ +export type BtcL1ReconcileRequest = { + /** Prefer settlementId; otherwise idempotencyKey. */ + settlementId?: string; + idempotencyKey?: string; + amountSats: number; + txId: string; + depositAddress: string; + instructionId: string; + recipientAddress: string; + /** On-chain cBTC mint tx hash. */ + chainTxHash: string; + btcUsdRate: number; + usdValue?: string; + fineractJournalRef?: string; + priceSource?: string; + priceAsOf?: string; + confirmations?: number; + valueDate?: string; +}; +export type BtcSettlementSnapshot = { + amountSats: number; + txId: string; + btcUsdRate: number; + usdValue: string; + depositAddress: string; + instructionId?: string; + priceSource?: string; + priceAsOf?: string; +}; +export type FiatLpSwapLeg = { + lpSymbol: string; + tokenIn: string; + tokenOut: string; + realSymbol: string; + amountIn: string; + amountOutQuoted?: string; + txHash?: string; + status: 'DRY_RUN' | 'QUOTED' | 'SUBMITTED' | 'SETTLED' | 'SKIPPED' | 'FAILED'; + error?: string; +}; export type SettlementRecord = { settlementId: string; idempotencyKey: string; @@ -95,6 +168,10 @@ export type SettlementRecord = { hybxPaymentId?: string; iso20022MessageId?: string; chainTxHash?: string; + /** c* → real liquidity swaps on Chain 138 */ + liquiditySwaps?: FiatLpSwapLeg[]; + /** L1 BTC custody + valuation snapshot at settle time */ + btcSettlement?: BtcSettlementSnapshot; verbiageDocument?: string; layerSteps?: ZBankLayerStep[]; errors: string[]; diff --git a/packages/settlement-core/src/btc-settlement.test.ts b/packages/settlement-core/src/btc-settlement.test.ts new file mode 100644 index 0000000..503d919 --- /dev/null +++ b/packages/settlement-core/src/btc-settlement.test.ts @@ -0,0 +1,28 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { + btcUsdToJournalAmount, + resolveBtcL1CustodyJournal, + satsToBtcDecimal, +} from './money-supply'; + +describe('BTC settlement helpers', () => { + it('converts sats to 8-decimal BTC', () => { + assert.equal(satsToBtcDecimal(100_000_000), '1.00000000'); + assert.equal(satsToBtcDecimal(1), '0.00000001'); + assert.equal(satsToBtcDecimal(0), '0.00000000'); + }); + + it('values sats at live BTC/USD for Fineract journal amount', () => { + assert.equal(btcUsdToJournalAmount(100_000_000, 95_000), '95000.00'); + assert.equal(btcUsdToJournalAmount(50_000_000, 100_000), '50000.00'); + assert.equal(btcUsdToJournalAmount(0, 100_000), '0.00'); + }); + + it('resolves T-BTC-L1-HO custody journal pair', () => { + const j = resolveBtcL1CustodyJournal(24); + assert.equal(j.debitGl, '12015'); + assert.equal(j.creditGl, '12424'); + assert.equal(j.memo, 'T-BTC-L1-HO'); + }); +}); diff --git a/packages/settlement-core/src/money-supply.ts b/packages/settlement-core/src/money-supply.ts index 0692c16..8f6a4d1 100644 --- a/packages/settlement-core/src/money-supply.ts +++ b/packages/settlement-core/src/money-supply.ts @@ -10,6 +10,8 @@ export const GL_CODES = { SETTLEMENT_SUSPENSE: '1000', TRADE_FINANCE_CONTINGENT: '1410', CRYPTO_TOKEN_LIABILITY: '2300', + BTC_L1_CUSTODY_HO: '12015', + BTC_INTEROFFICE_O24: '12424', } as const; /** Policy caps: M0→M1 (1.2×), M1→M2 (5×), M2→M3 token mint, M4 near-money rail */ @@ -191,3 +193,32 @@ export function resolveM4OutboundJournal(moneyLayers: MoneyLayer[], gl: { } return { debitGl: GL_CODES.M2_BROAD, creditGl: suspense }; } + +/** Convert satoshis to BTC decimal string (8 places). */ +export function satsToBtcDecimal(amountSats: number): string { + const sats = Math.max(0, Math.floor(Number(amountSats) || 0)); + return (sats / 1e8).toFixed(8); +} + +/** USD journal amount for Fineract = sats × BTC/USD rate (2 decimal places). */ +export function btcUsdToJournalAmount(amountSats: number, btcUsdRate: number): string { + const sats = Math.max(0, Number(amountSats) || 0); + const rate = Number(btcUsdRate) || 0; + if (sats <= 0 || rate <= 0) return '0.00'; + return ((sats / 1e8) * rate).toFixed(2); +} + +/** IPSAS journal pair for L1 BTC custody confirm (T-BTC-L1-HO): Dr 12015 / Cr 12424. */ +export function resolveBtcL1CustodyJournal(_officeId?: number): { + debitGl: string; + creditGl: string; + narrative: string; + memo: string; +} { + return { + debitGl: GL_CODES.BTC_L1_CUSTODY_HO, + creditGl: GL_CODES.BTC_INTEROFFICE_O24, + narrative: 'L1 BTC custody confirm — Dr HO digital asset reserve / Cr Office 24 interoffice BTC', + memo: 'T-BTC-L1-HO', + }; +} diff --git a/packages/settlement-core/src/types.ts b/packages/settlement-core/src/types.ts index a992136..9a74818 100644 --- a/packages/settlement-core/src/types.ts +++ b/packages/settlement-core/src/types.ts @@ -114,6 +114,84 @@ export type ZBankM0M4Request = { tokenLineId?: string; }; +/** Swap fiat-backed LP token (c*) → real fund on Chain 138 (USDT/USDC/WETH). */ +export type FiatLpToLiquidityRequest = { + idempotencyKey: string; + officeId?: number; + /** Wallet holding c* LP tokens (treasury or recipient after mint). */ + holderAddress: string; + /** Receiver of real USDT/USDC/WETH after swap; defaults to holderAddress. */ + recipientAddress?: string; + /** Single LP symbol (e.g. cUSDT); omit with convertAll for full portfolio. */ + symbol?: string; + amount?: string; + convertAll?: boolean; + maxSlippageBps?: number; + dryRun?: boolean; +}; + +/** L1 Bitcoin deposit confirmed → Fineract custody journal + cBTC mint. */ +export type BtcL1SettlementRequest = { + idempotencyKey: string; + /** Sender office (default 24). */ + officeId?: number; + instructionId: string; + txId: string; + depositAddress: string; + amountSats: number; + confirmations: number; + /** Chain 138 vault for cBTC mint. */ + recipientAddress: string; + valueDate?: string; +}; + +/** + * Mark a FAILED/partial BTC L1 settle as SETTLED when books + mint already + * happened out-of-band (no re-journal / no remint). + */ +export type BtcL1ReconcileRequest = { + /** Prefer settlementId; otherwise idempotencyKey. */ + settlementId?: string; + idempotencyKey?: string; + amountSats: number; + txId: string; + depositAddress: string; + instructionId: string; + recipientAddress: string; + /** On-chain cBTC mint tx hash. */ + chainTxHash: string; + btcUsdRate: number; + usdValue?: string; + fineractJournalRef?: string; + priceSource?: string; + priceAsOf?: string; + confirmations?: number; + valueDate?: string; +}; + +export type BtcSettlementSnapshot = { + amountSats: number; + txId: string; + btcUsdRate: number; + usdValue: string; + depositAddress: string; + instructionId?: string; + priceSource?: string; + priceAsOf?: string; +}; + +export type FiatLpSwapLeg = { + lpSymbol: string; + tokenIn: string; + tokenOut: string; + realSymbol: string; + amountIn: string; + amountOutQuoted?: string; + txHash?: string; + status: 'DRY_RUN' | 'QUOTED' | 'SUBMITTED' | 'SETTLED' | 'SKIPPED' | 'FAILED'; + error?: string; +}; + export type SettlementRecord = { settlementId: string; idempotencyKey: string; @@ -123,6 +201,10 @@ export type SettlementRecord = { hybxPaymentId?: string; iso20022MessageId?: string; chainTxHash?: string; + /** c* → real liquidity swaps on Chain 138 */ + liquiditySwaps?: FiatLpSwapLeg[]; + /** L1 BTC custody + valuation snapshot at settle time */ + btcSettlement?: BtcSettlementSnapshot; verbiageDocument?: string; layerSteps?: ZBankLayerStep[]; errors: string[]; diff --git a/scripts/custody/package-lock.json b/scripts/custody/package-lock.json new file mode 100644 index 0000000..a7408d6 --- /dev/null +++ b/scripts/custody/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "custody", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@noble/ed25519": "^3.1.0" + } + }, + "node_modules/@noble/ed25519": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.1.0.tgz", + "integrity": "sha512-pfcObRY3CtvwfaG9Mt5XqZdKmAQppl37tHUeuBhDUbiwJBCVY4/A4lbMvb1xKhMDx96AqAqZpMWuBX1HulhX4g==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + } + } +} diff --git a/scripts/custody/package.json b/scripts/custody/package.json new file mode 100644 index 0000000..9208c37 --- /dev/null +++ b/scripts/custody/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@noble/ed25519": "^3.1.0" + } +} diff --git a/scripts/custody/smoke-dfns-cobo.mjs b/scripts/custody/smoke-dfns-cobo.mjs new file mode 100644 index 0000000..d37b6a4 --- /dev/null +++ b/scripts/custody/smoke-dfns-cobo.mjs @@ -0,0 +1,234 @@ +#!/usr/bin/env node +/** + * Smoke-test Dfns + Cobo credentials from secrets/*. + * Usage: node scripts/custody/smoke-dfns-cobo.mjs + * Does not print secrets — only HTTP status and non-sensitive fields. + */ +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(__dirname, '../..'); + +function loadEnvFile(rel) { + const p = path.join(root, rel); + if (!fs.existsSync(p)) throw new Error(`Missing ${rel}`); + const out = {}; + for (const line of fs.readFileSync(p, 'utf8').split(/\r?\n/)) { + if (!line || line.trim().startsWith('#')) continue; + const i = line.indexOf('='); + if (i < 0) continue; + out[line.slice(0, i).trim()] = line.slice(i + 1).trim(); + } + return out; +} + +function redact(s) { + if (!s) return '(empty)'; + if (s.length <= 12) return '***'; + return `${s.slice(0, 6)}…${s.slice(-4)} (len=${s.length})`; +} + +async function testDfns(dfns) { + const base = (dfns.DFNS_API_BASE_URL || 'https://api.dfns.io').replace(/\/$/, ''); + const token = dfns.DFNS_AUTH_TOKEN; + if (!token) throw new Error('DFNS_AUTH_TOKEN missing'); + + const pemPath = path.isAbsolute(dfns.DFNS_PRIVATE_KEY_PATH) + ? dfns.DFNS_PRIVATE_KEY_PATH + : path.join(root, dfns.DFNS_PRIVATE_KEY_PATH || 'secrets/dfns/rsa2048.pem'); + const pemOk = fs.existsSync(pemPath); + const pem = pemOk ? fs.readFileSync(pemPath, 'utf8') : ''; + const pemLooksPrivate = pem.includes('BEGIN PRIVATE KEY') || pem.includes('BEGIN RSA PRIVATE KEY'); + + // Read-only: list wallets (Bearer only) + const res = await fetch(`${base}/wallets?limit=5`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + }); + const text = await res.text(); + let body; + try { + body = JSON.parse(text); + } catch { + body = { raw: text.slice(0, 200) }; + } + + const items = body?.items ?? body?.wallets ?? []; + return { + ok: res.status >= 200 && res.status < 300, + http: res.status, + orgId: dfns.DFNS_ORG_ID || body?.items?.[0]?.orgId, + credIdPresent: Boolean(dfns.DFNS_CRED_ID), + credIdPrefix: redact(dfns.DFNS_CRED_ID), + tokenPrefix: redact(token), + privateKeyPem: pemOk && pemLooksPrivate ? 'present' : 'MISSING_OR_INVALID', + walletCount: Array.isArray(items) ? items.length : null, + sampleNetworks: Array.isArray(items) + ? items.slice(0, 3).map((w) => w.network || w.chain || w.id).filter(Boolean) + : [], + error: res.ok ? undefined : body?.error?.message || body?.message || text.slice(0, 180), + }; +} + +async function testCobo(cobo) { + const base = (cobo.COBO_API_BASE_URL || 'https://api.dev.cobo.com/v2').replace(/\/$/, ''); + const apiKey = cobo.COBO_API_KEY; + const apiSecret = cobo.COBO_API_SECRET; + if (!apiKey || !apiSecret) throw new Error('COBO_API_KEY / COBO_API_SECRET missing'); + + // Prefer @noble/ed25519 if available; else tweetnacl; else pure crypto createPrivateKey if possible + let signEd25519; + try { + const require = createRequire(import.meta.url); + const noblePath = path.join(root, 'services/token-aggregation/node_modules/@noble/ed25519'); + if (fs.existsSync(noblePath)) { + const ed = require(noblePath); + signEd25519 = async (msg, sk) => { + if (ed.signAsync) return Buffer.from(await ed.signAsync(msg, sk)); + return Buffer.from(ed.sign(msg, sk)); + }; + } + } catch { + /* fall through */ + } + if (!signEd25519) { + try { + const require = createRequire(import.meta.url); + const nacl = require(path.join(root, 'services/token-aggregation/node_modules/tweetnacl')); + signEd25519 = async (msg, sk) => Buffer.from(nacl.sign.detached(msg, sk)); + } catch { + /* use openssl-free path via dynamic import of noble from npm if needed */ + } + } + + // Install-free: use Node crypto for Ed25519 if secret is 32-byte seed + if (!signEd25519) { + signEd25519 = async (msg, skHex) => { + // Node 20+: createPrivateKey from raw JWK-like — use sodium via webcrypto experimental + // Fallback: spawn openssl not available for raw hex easily. + // Use @noble via dynamic CDN-less: implement with crypto.createPrivateKey PKCS8 + throw new Error('Need @noble/ed25519 or tweetnacl — run npm install in scripts/custody'); + }; + } + + const method = 'GET'; + const pathPart = '/v2/wallets'; + const params = 'limit=5'; + const body = ''; + const nonce = String(Date.now()); + const strToSign = `${method}|${pathPart}|${nonce}|${params}|${body}`; + const contentHash = crypto.createHash('sha256').update( + crypto.createHash('sha256').update(strToSign, 'utf8').digest(), + ).digest(); + + const sk = Buffer.from(apiSecret, 'hex'); + if (sk.length !== 32) { + return { + ok: false, + http: 0, + error: `API secret hex length ${sk.length} (expected 32 bytes / 64 hex)`, + apiKeyPrefix: redact(apiKey), + }; + } + + // tweetnacl needs 64-byte secret key (seed+pub); @noble accepts 32-byte seed + let signatureHex; + try { + const require = createRequire(import.meta.url); + let ed; + try { + ed = require('@noble/ed25519'); + } catch { + ed = require(path.join(root, 'node_modules/@noble/ed25519')); + } + // noble v2 uses hashes.sha512 + if (ed.etc?.sha512Async == null && ed.hashes == null) { + const { createHash } = await import('crypto'); + ed.etc = ed.etc || {}; + ed.etc.sha512Sync = (...m) => createHash('sha512').update(Buffer.concat(m.map((x) => Buffer.from(x)))).digest(); + } + const sig = await (ed.signAsync ? ed.signAsync(contentHash, sk) : Promise.resolve(ed.sign(contentHash, sk))); + signatureHex = Buffer.from(sig).toString('hex'); + } catch (e1) { + try { + const require = createRequire(import.meta.url); + const nacl = require('tweetnacl'); + // Derive full secret key from seed + const kp = nacl.sign.keyPair.fromSeed(sk); + signatureHex = Buffer.from(nacl.sign.detached(contentHash, kp.secretKey)).toString('hex'); + } catch (e2) { + return { + ok: false, + http: 0, + error: `Ed25519 sign unavailable: ${e1.message}; ${e2.message}`, + apiKeyPrefix: redact(apiKey), + }; + } + } + + // Host already includes /v2 — request URL is base + /wallets + const url = `${base}/wallets?${params}`; + const res = await fetch(url, { + method: 'GET', + headers: { + 'Biz-Api-Key': apiKey, + 'Biz-Api-Nonce': nonce, + 'Biz-Api-Signature': signatureHex, + Accept: 'application/json', + }, + }); + const text = await res.text(); + let bodyJson; + try { + bodyJson = JSON.parse(text); + } catch { + bodyJson = { raw: text.slice(0, 200) }; + } + + const data = bodyJson?.data ?? bodyJson; + const list = data?.data ?? data?.wallets ?? (Array.isArray(data) ? data : []); + + return { + ok: res.status >= 200 && res.status < 300, + http: res.status, + base, + apiKeyPrefix: redact(apiKey), + expires: cobo.COBO_EXPIRES, + walletCount: Array.isArray(list) ? list.length : data?.total ?? null, + error: res.ok + ? undefined + : bodyJson?.error_message || bodyJson?.errorMessage || bodyJson?.message || text.slice(0, 220), + }; +} + +const dfns = loadEnvFile('secrets/dfns/credentials.env'); +const cobo = loadEnvFile('secrets/cobo/credentials.env'); + +const dfnsResult = await testDfns(dfns); +const coboResult = await testCobo(cobo); + +console.log( + JSON.stringify( + { + saved: { + dfnsEnv: 'secrets/dfns/credentials.env', + dfnsPem: 'secrets/dfns/rsa2048.pem', + coboEnv: 'secrets/cobo/credentials.env', + gitignored: true, + }, + dfns: dfnsResult, + cobo: coboResult, + bothOk: dfnsResult.ok && coboResult.ok, + }, + null, + 2, + ), +); + +process.exit(dfnsResult.ok && coboResult.ok ? 0 : 1); diff --git a/scripts/deployment/btc-l1-settlement-e2e.mjs b/scripts/deployment/btc-l1-settlement-e2e.mjs new file mode 100644 index 0000000..e605d75 --- /dev/null +++ b/scripts/deployment/btc-l1-settlement-e2e.mjs @@ -0,0 +1,120 @@ +#!/usr/bin/env node +/** + * BTC L1 settlement E2E (regtest or simulated confirmed deposit). + * + * Modes: + * --simulate Skip bitcoind; post a synthetic confirmed deposit to local settlement + * (default) Prefer bitcoind RPC if BITCOIND_RPC_URL reachable + * + * Prerequisites: settlement-middleware on SETTLEMENT_MIDDLEWARE_URL (default :3011) + * live BTC/USD via TOKEN_AGGREGATION_URL + */ +import { randomUUID } from 'crypto'; + +const settleBase = (process.env.SETTLEMENT_MIDDLEWARE_URL || 'http://127.0.0.1:3011').replace(/\/$/, ''); +const apiKey = process.env.OMNL_API_KEY || ''; +const simulate = process.argv.includes('--simulate'); +const amountSats = Number(process.env.BTC_E2E_AMOUNT_SATS || 100_000); // 0.001 BTC +const recipient = + process.env.BTC_E2E_RECIPIENT || + process.env.ALI_GENESIS_ADDRESS || + '0xa55A4B57A91561e9df5a883D4883Bd4b1a7C4882'; + +const headers = { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), +}; + +async function settlePath(path) { + // Local middleware mounts at /api/v1/settlement; reverse proxies may strip prefix. + const candidates = [ + `${settleBase}/api/v1/settlement${path}`, + `${settleBase}${path}`, + ]; + let lastErr; + for (const url of candidates) { + try { + const res = await fetch(url, { headers }); + if (res.status !== 404) return { url, res }; + } catch (e) { + lastErr = e; + } + } + throw lastErr || new Error(`No settlement route for ${path}`); +} + +async function postSettle(body) { + const candidates = [ + `${settleBase}/api/v1/settlement/btc/l1-settle`, + `${settleBase}/btc/l1-settle`, + ]; + let last; + for (const url of candidates) { + const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) }); + const text = await res.text(); + let json; + try { + json = JSON.parse(text); + } catch { + json = { raw: text }; + } + last = { url, status: res.status, json }; + if (res.status !== 404) return last; + } + return last; +} + +async function main() { + const health = await settlePath('/health'); + const healthBody = await health.res.json(); + console.log('health', health.url, healthBody.status || healthBody); + + const instructionId = randomUUID(); + const txId = simulate + ? `sim-${randomUUID().replace(/-/g, '').slice(0, 32)}` + : process.env.BTC_E2E_TXID || `regtest-${Date.now().toString(16)}`; + const depositAddress = + process.env.BTC_E2E_DEPOSIT_ADDRESS || `bcrt1q${instructionId.replace(/-/g, '').slice(0, 38)}`; + + const body = { + idempotencyKey: `btc-e2e-${txId}`, + instructionId, + txId, + depositAddress, + amountSats, + confirmations: Number(process.env.BTC_CONFIRMATIONS_REQUIRED || 6), + recipientAddress: recipient, + officeId: 24, + }; + + console.log('posting', { amountSats, recipient, simulate, txId }); + const result = await postSettle(body); + console.log(JSON.stringify(result, null, 2)); + + const ledger = await settlePath('/btc/ledger'); + if (ledger.res.ok) { + console.log('ledger', await ledger.res.json()); + } else { + console.log('ledger_status', ledger.res.status); + } + + if (result.status >= 400 && result.json?.phase !== 'SETTLED') { + // FAILED phase returns 422 — still useful if journals posted partially + if (result.json?.phase === 'FAILED') { + console.error('Settlement FAILED:', result.json.errors); + process.exit(2); + } + process.exit(1); + } + if (result.json?.phase && result.json.phase !== 'SETTLED') { + console.error('Unexpected phase', result.json.phase); + process.exit(2); + } + console.log('E2E_OK', result.json?.settlementId, result.json?.phase); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/scripts/deployment/btc-regtest-bootstrap.mjs b/scripts/deployment/btc-regtest-bootstrap.mjs new file mode 100644 index 0000000..e674474 --- /dev/null +++ b/scripts/deployment/btc-regtest-bootstrap.mjs @@ -0,0 +1,93 @@ +#!/usr/bin/env node +import http from 'http'; +import fs from 'fs'; +import path from 'path'; + +const RPC_USER = process.env.BITCOIND_RPC_USER || 'omnl'; +const RPC_PASS = process.env.BITCOIND_RPC_PASS || 'omnl-regtest-local'; +const RPC_PORT = Number(process.env.BITCOIND_RPC_PORT || 18443); + +function rpc(method, params = []) { + return new Promise((resolve, reject) => { + const body = JSON.stringify({ jsonrpc: '1.0', id: 'e2e', method, params }); + const req = http.request( + { + hostname: '127.0.0.1', + port: RPC_PORT, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Basic ' + Buffer.from(`${RPC_USER}:${RPC_PASS}`).toString('base64'), + 'Content-Length': Buffer.byteLength(body), + }, + }, + (res) => { + let d = ''; + res.on('data', (c) => (d += c)); + res.on('end', () => { + try { + const j = JSON.parse(d); + if (j.error) reject(new Error(JSON.stringify(j.error))); + else resolve(j.result); + } catch (e) { + reject(e); + } + }); + }, + ); + req.on('error', reject); + req.write(body); + req.end(); + }); +} + +function upsertEnv(file, key, value) { + if (!fs.existsSync(file)) return; + let txt = fs.readFileSync(file, 'utf8'); + const line = `${key}=${value}`; + if (new RegExp(`^${key}=`, 'm').test(txt)) { + txt = txt.replace(new RegExp(`^${key}=.*$`, 'm'), line); + } else { + txt += `\n${line}\n`; + } + fs.writeFileSync(file, txt); +} + +const info = await rpc('getblockchaininfo'); +console.log(`chain=${info.chain} blocks=${info.blocks}`); +try { + await rpc('createwallet', ['omnl']); +} catch { + /* exists */ +} +try { + await rpc('loadwallet', ['omnl']); +} catch { + /* loaded */ +} + +const addr = await rpc('getnewaddress', ['reserve', 'bech32']); +if (info.blocks < 101) { + await rpc('generatetoaddress', [101 - info.blocks, addr]); +} +const bal = await rpc('getbalance'); +console.log(`REGTEST_READY addr=${addr} bal=${bal}`); + +const dep = await rpc('getnewaddress', ['omnl-e2e-deposit', 'bech32']); +const txid = await rpc('sendtoaddress', [dep, 0.001]); +await rpc('generatetoaddress', [6, addr]); +const tx = await rpc('gettransaction', [txid]); +console.log(`DEPOSIT txid=${txid} conf=${tx.confirmations} to=${dep}`); + +upsertEnv(path.resolve('.env'), 'BTC_RESERVE_ADDRESSES', addr); +upsertEnv(path.resolve('config/deployment-omnl.production.env'), 'BTC_RESERVE_ADDRESSES', addr); + +console.log( + JSON.stringify({ + reserve: addr, + deposit: dep, + txid, + confirmations: tx.confirmations, + amountSats: 100_000, + }), +); diff --git a/scripts/deployment/deploy-omnl-bank-production.sh b/scripts/deployment/deploy-omnl-bank-production.sh index 00ee8f2..d69ca1b 100644 --- a/scripts/deployment/deploy-omnl-bank-production.sh +++ b/scripts/deployment/deploy-omnl-bank-production.sh @@ -99,7 +99,7 @@ build_pkg() { for dir in packages/integration-foundation packages/settlement-core \ services/token-aggregation services/settlement-middleware services/dbis-exchange \ - services/swift-listener; do + services/btc-intake services/swift-listener; do if [[ "$dir" == "services/swift-listener" ]]; then build_pkg "$dir" || log "WARN: swift-listener build skipped — SWIFT inbound may use existing dist" else @@ -170,6 +170,15 @@ start_svc() { AZURE_OPENAI_DEPLOYMENT="${AZURE_OPENAI_DEPLOYMENT:-}" \ AZURE_OPENAI_API_VERSION="${AZURE_OPENAI_API_VERSION:-2024-02-15-preview}" \ OMNL_MINT_OPERATOR_PRIVATE_KEY="${OMNL_MINT_OPERATOR_PRIVATE_KEY:-}" \ + OMNL_REQUIRE_LIVE_BTC_PRICE="${OMNL_REQUIRE_LIVE_BTC_PRICE:-1}" \ + BITCOIND_RPC_URL="${BITCOIND_RPC_URL:-}" \ + BITCOIND_RPC_USER="${BITCOIND_RPC_USER:-}" \ + BITCOIND_RPC_PASS="${BITCOIND_RPC_PASS:-}" \ + BTC_NETWORK="${BTC_NETWORK:-mainnet}" \ + BTC_RESERVE_ADDRESSES="${BTC_RESERVE_ADDRESSES:-}" \ + BTC_CONFIRMATIONS_REQUIRED="${BTC_CONFIRMATIONS_REQUIRED:-6}" \ + BTC_MAX_OUTSTANDING_SATS="${BTC_MAX_OUTSTANDING_SATS:-}" \ + SETTLEMENT_MIDDLEWARE_URL="${SETTLEMENT_MIDDLEWARE_URL:-http://127.0.0.1:3011}" \ bash -c "$cmd" >"$LOG_DIR/$name.log" 2>&1 & echo $! >"$LOG_DIR/$name.pid" } @@ -181,6 +190,12 @@ sleep 2 start_svc dbis-exchange services/dbis-exchange 3012 "npm run start" sleep 2 +if [[ "${OMNL_BTC_INTAKE_START:-1}" == "1" ]] && [[ -n "${BITCOIND_RPC_URL:-}" ]]; then + log "Starting btc-intake..." + start_svc btc-intake services/btc-intake 3013 "npm run start" + sleep 2 +fi + if [[ "${OMNL_SWIFT_LISTENER_START:-1}" == "1" ]]; then log "Starting SWIFT listener..." bash "$REPO_DIR/scripts/deployment/start-swift-listener-production.sh" || log "WARN: SWIFT listener start failed" @@ -200,6 +215,10 @@ curl -sf "http://127.0.0.1:3011/api/v1/settlement/money-supply/public" | head -c echo curl -sf "http://127.0.0.1:3012/api/v1/exchange/health" | head -c 200 || true echo +if [[ "${OMNL_BTC_INTAKE_START:-1}" == "1" ]] && [[ -n "${BITCOIND_RPC_URL:-}" ]]; then + curl -sf "http://127.0.0.1:3013/health" | head -c 200 || log "WARN: btc-intake :3013 not reachable" + echo +fi curl -sf -o /dev/null -w "frontend=%{http_code}\n" "http://127.0.0.1:$FRONTEND_PORT/central-bank" curl -sf -o /dev/null -w "hub=%{http_code}\n" "http://127.0.0.1:$FRONTEND_PORT/hub" curl -sf "http://127.0.0.1:8788/health" 2>/dev/null | head -c 80 || log "WARN: SWIFT listener :8788 not reachable" diff --git a/scripts/deployment/ensure-cbtc-pmm-pools.mjs b/scripts/deployment/ensure-cbtc-pmm-pools.mjs new file mode 100644 index 0000000..6c293c0 --- /dev/null +++ b/scripts/deployment/ensure-cbtc-pmm-pools.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node +/** + * Ensure cBTC DODO PMM pools exist on Chain 138 and print CHAIN138_POOL_CBTC_* env lines. + * Requires POOL_MANAGER_ROLE on DODOPMMIntegration (PRIVATE_KEY / OMNL_MINT_OPERATOR_PRIVATE_KEY). + */ +import fs from 'fs'; +import path from 'path'; +import { Contract, JsonRpcProvider, Wallet, ZeroAddress } from 'ethers'; + +const rpc = process.env.RPC_URL_138 || process.env.CHAIN_138_RPC_URL || 'https://rpc.d-bis.org'; +const pk = + process.env.PRIVATE_KEY?.trim() || + process.env.OMNL_MINT_OPERATOR_PRIVATE_KEY?.trim(); +const integrationAddr = + process.env.DODO_PMM_INTEGRATION_ADDRESS || + process.env.CHAIN_138_DODO_PMM_INTEGRATION || + '0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895'; + +const CBTC = '0xe94260c555ac1d9d3cc9e1632883452ebdf0082e'; +const PAIRS = [ + { env: 'CHAIN138_POOL_CBTC_CUSDT', quote: '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22', sym: 'cUSDT' }, + { env: 'CHAIN138_POOL_CBTC_CUSDC', quote: '0xf22258f57794CC8E06237084b353Ab30fFfa640b', sym: 'cUSDC' }, + { env: 'CHAIN138_POOL_CBTC_CXAUC', quote: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b', sym: 'cXAUC' }, +]; + +const LP_FEE = 3n; +const INITIAL_PRICE = 10n ** 18n; +const K_FACTOR = 5n * 10n ** 17n; +const ENABLE_TWAP = false; + +if (!pk) { + console.error('PRIVATE_KEY or OMNL_MINT_OPERATOR_PRIVATE_KEY required'); + process.exit(1); +} + +const provider = new JsonRpcProvider(rpc); +const wallet = new Wallet(pk, provider); +const abi = [ + 'function pools(address,address) view returns (address)', + 'function createPool(address,address,uint256,uint256,uint256,bool) returns (address)', + 'function POOL_MANAGER_ROLE() view returns (bytes32)', + 'function hasRole(bytes32,address) view returns (bool)', +]; +const integration = new Contract(integrationAddr, abi, wallet); + +function upsertEnv(file, key, value) { + if (!fs.existsSync(file)) return; + let txt = fs.readFileSync(file, 'utf8'); + const line = `${key}=${value}`; + if (new RegExp(`^${key}=`, 'm').test(txt)) txt = txt.replace(new RegExp(`^${key}=.*$`, 'm'), line); + else txt += `\n${line}\n`; + fs.writeFileSync(file, txt); +} + +const role = await integration.POOL_MANAGER_ROLE(); +const canManage = await integration.hasRole(role, wallet.address); +console.log(JSON.stringify({ operator: wallet.address, canManage, integration: integrationAddr }, null, 2)); + +const results = {}; +for (const pair of PAIRS) { + let pool = await integration.pools(CBTC, pair.quote); + if (!pool || pool === ZeroAddress) { + pool = await integration.pools(pair.quote, CBTC); + } + if (pool && pool !== ZeroAddress) { + console.log(`EXISTS ${pair.sym} -> ${pool}`); + results[pair.env] = pool; + continue; + } + if (!canManage) { + console.error(`MISSING ${pair.sym} and operator lacks POOL_MANAGER_ROLE`); + results[pair.env] = ''; + continue; + } + console.log(`CREATE ${pair.sym}...`); + const tx = await integration.createPool(CBTC, pair.quote, LP_FEE, INITIAL_PRICE, K_FACTOR, ENABLE_TWAP); + const receipt = await tx.wait(1); + pool = await integration.pools(CBTC, pair.quote); + console.log(`CREATED ${pair.sym} -> ${pool} tx=${receipt.hash}`); + results[pair.env] = pool; +} + +for (const [k, v] of Object.entries(results)) { + if (!v) continue; + upsertEnv(path.resolve('.env'), k, v); + upsertEnv(path.resolve('config/deployment-omnl.production.env'), k, v); + console.log(`${k}=${v}`); +} + +console.log(JSON.stringify({ results }, null, 2)); diff --git a/scripts/deployment/hot-cbtc-liquidity-1000.mjs b/scripts/deployment/hot-cbtc-liquidity-1000.mjs new file mode 100644 index 0000000..710a68f --- /dev/null +++ b/scripts/deployment/hot-cbtc-liquidity-1000.mjs @@ -0,0 +1,151 @@ +#!/usr/bin/env node +/** + * Bring cBTC/cUSDT PMM pool base reserve to >= TARGET_CBTC (default 1000). + * + * Env: + * OMNL_MINT_OPERATOR_PRIVATE_KEY (required) + * RPC_URL_138 (default https://rpc.d-bis.org) + * HOT_CBTC_TARGET (default 1000) + * HOT_CBTC_EXECUTE=1 (required to mint + addLiquidity) + * BTC_USD_PRICE (optional override for quote sizing) + * + * Usage: + * HOT_CBTC_EXECUTE=1 node scripts/deployment/hot-cbtc-liquidity-1000.mjs + */ +import { Contract, JsonRpcProvider, Wallet, MaxUint256, parseUnits, formatUnits } from 'ethers'; + +const CBTC = '0xe94260c555ac1d9d3cc9e1632883452ebdf0082e'; +const CUSDT = '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22'; +const INTEGRATION = '0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895'; +const POOL_USDT = process.env.CHAIN138_POOL_CBTC_CUSDT || '0x481CcE1046e1F1ab13BC37f0f374bb5ff75F3a8d'; +const TARGET = Number(process.env.HOT_CBTC_TARGET || '1000'); +const EXECUTE = process.env.HOT_CBTC_EXECUTE === '1'; + +const ercAbi = [ + 'function balanceOf(address) view returns (uint256)', + 'function approve(address,uint256) returns (bool)', + 'function allowance(address,address) view returns (uint256)', + 'function mint(address,uint256)', + 'function decimals() view returns (uint8)', +]; +const poolAbi = [ + 'function _BASE_RESERVE_() view returns (uint256)', + 'function _QUOTE_RESERVE_() view returns (uint256)', +]; +const integAbi = ['function addLiquidity(address,uint256,uint256) returns (uint256,uint256,uint256)']; + +async function main() { + const pk = process.env.OMNL_MINT_OPERATOR_PRIVATE_KEY?.trim(); + if (!pk) throw new Error('OMNL_MINT_OPERATOR_PRIVATE_KEY required'); + const rpc = process.env.RPC_URL_138 || process.env.CHAIN_138_RPC_URL || 'https://rpc.d-bis.org'; + const provider = new JsonRpcProvider(rpc); + const wallet = new Wallet(pk, provider); + + const cbtc = new Contract(CBTC, ercAbi, wallet); + const cusdt = new Contract(CUSDT, ercAbi, wallet); + const integ = new Contract(INTEGRATION, integAbi, wallet); + const pool = new Contract(POOL_USDT, poolAbi, provider); + + const targetWei = parseUnits(String(TARGET), 8); + const base = await pool._BASE_RESERVE_(); + const quote = await pool._QUOTE_RESERVE_(); + console.log( + JSON.stringify( + { + pool: POOL_USDT, + base: formatUnits(base, 8), + quote: formatUnits(quote, 6), + target: TARGET, + execute: EXECUTE, + operator: wallet.address, + }, + null, + 2, + ), + ); + + if (base >= targetWei) { + console.log('OK pool already >= target'); + return; + } + + const needBase = targetWei - base; + // Match current pool ratio; fallback to env BTC_USD_PRICE or ~65000 + const price = + Number(process.env.BTC_USD_PRICE || '') || + (quote > 0n && base > 0n + ? Number(formatUnits(quote, 6)) / Number(formatUnits(base, 8)) + : 65000); + const needQuote = parseUnits((Number(formatUnits(needBase, 8)) * price).toFixed(6), 6); + + let opCbtc = await cbtc.balanceOf(wallet.address); + const opUsdt = await cusdt.balanceOf(wallet.address); + console.log( + JSON.stringify( + { + needBase: formatUnits(needBase, 8), + needQuote: formatUnits(needQuote, 6), + price, + opCbtc: formatUnits(opCbtc, 8), + opUsdt: formatUnits(opUsdt, 6), + }, + null, + 2, + ), + ); + + if (!EXECUTE) { + console.log('DRY_RUN — set HOT_CBTC_EXECUTE=1 to mint + addLiquidity'); + return; + } + + if (opCbtc < needBase) { + const mintAmt = needBase - opCbtc + parseUnits('0.01', 8); + console.log('minting cBTC', formatUnits(mintAmt, 8)); + const tx = await cbtc.mint(wallet.address, mintAmt); + console.log('mintTx', tx.hash); + await tx.wait(); + opCbtc = await cbtc.balanceOf(wallet.address); + } + + if (opUsdt < needQuote) { + throw new Error( + `Insufficient cUSDT: have ${formatUnits(opUsdt, 6)} need ${formatUnits(needQuote, 6)}`, + ); + } + + async function ensureAllow(token, spender, needed) { + const a = await token.allowance(wallet.address, spender); + if (a < needed) { + const tx = await token.approve(spender, MaxUint256); + await tx.wait(); + } + } + + await ensureAllow(cbtc, INTEGRATION, needBase); + await ensureAllow(cusdt, INTEGRATION, needQuote); + + console.log('addLiquidity', formatUnits(needBase, 8), 'cBTC +', formatUnits(needQuote, 6), 'cUSDT'); + const lpTx = await integ.addLiquidity(POOL_USDT, needBase, needQuote); + console.log('lpTx', lpTx.hash); + await lpTx.wait(); + + const baseAfter = await pool._BASE_RESERVE_(); + const quoteAfter = await pool._QUOTE_RESERVE_(); + console.log( + JSON.stringify( + { + baseAfter: formatUnits(baseAfter, 8), + quoteAfter: formatUnits(quoteAfter, 6), + hot: baseAfter >= targetWei, + }, + null, + 2, + ), + ); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/scripts/deployment/seed-btc-custody-gl-accounts.mjs b/scripts/deployment/seed-btc-custody-gl-accounts.mjs new file mode 100644 index 0000000..d424bf4 --- /dev/null +++ b/scripts/deployment/seed-btc-custody-gl-accounts.mjs @@ -0,0 +1,109 @@ +#!/usr/bin/env node +/** + * Ensure Fineract GL accounts for L1 BTC custody exist: + * 12015 — Digital asset reserves — BTC (L1 mainnet custody) [ASSET] + * 12424 — Office 24 — digital asset custody — BTC [ASSET] + * + * Usage: + * node --env-file=.env scripts/deployment/seed-btc-custody-gl-accounts.mjs + * node --env-file=.env scripts/deployment/seed-btc-custody-gl-accounts.mjs --dry-run + */ +const dryRun = process.argv.includes('--dry-run'); + +const ACCOUNTS = [ + { + glCode: '12015', + name: 'Digital asset reserves — BTC (L1 mainnet custody)', + description: 'HO native BTC L1 custody; debit on confirmed receipt to registered HO address', + type: 1, // ASSET + usage: 1, // DETAIL + manualEntriesAllowed: true, + }, + { + glCode: '12424', + name: 'Office 24 — digital asset custody — BTC (sent to HO)', + description: 'Credit on sender-office leg when HO confirms L1 BTC from HOSPITALLERS-24', + type: 1, + usage: 1, + manualEntriesAllowed: true, + }, +]; + +const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, ''); +const tenant = process.env.OMNL_FINERACT_TENANT || 'omnl'; +const user = + process.env.OMNL_FINERACT_USER?.trim() || + process.env.OMNL_FINERACT_USERNAME?.trim() || + 'ali_hospitallers_tenant'; +const pass = process.env.OMNL_FINERACT_PASSWORD || ''; + +if (!base || !pass) { + console.error('Set OMNL_FINERACT_BASE_URL and OMNL_FINERACT_PASSWORD'); + process.exit(1); +} + +const auth = Buffer.from(`${user}:${pass}`).toString('base64'); +const headers = { + Authorization: `Basic ${auth}`, + 'Fineract-Platform-TenantId': tenant, + 'Content-Type': 'application/json', + Accept: 'application/json', +}; + +async function findByCode(glCode) { + const res = await fetch(`${base}/glaccounts?glCode=${encodeURIComponent(glCode)}`, { headers }); + if (!res.ok) { + // fallback list + const all = await fetch(`${base}/glaccounts?limit=1000`, { headers }); + if (!all.ok) throw new Error(`GL list failed (${all.status})`); + const data = await all.json(); + const list = Array.isArray(data) ? data : data.pageItems || []; + return list.find((r) => String(r.glCode) === glCode) || null; + } + const data = await res.json(); + const list = Array.isArray(data) ? data : data.pageItems || []; + return list.find((r) => String(r.glCode) === glCode) || list[0] || null; +} + +async function ensureAccount(spec) { + const existing = await findByCode(spec.glCode); + if (existing?.id) { + return { glCode: spec.glCode, status: 'exists', id: existing.id, name: existing.name }; + } + if (dryRun) { + return { glCode: spec.glCode, status: 'would_create', payload: spec }; + } + const res = await fetch(`${base}/glaccounts`, { + method: 'POST', + headers, + body: JSON.stringify(spec), + }); + const body = await res.text(); + if (!res.ok) { + throw new Error(`Create ${spec.glCode} failed (${res.status}): ${body}`); + } + let parsed; + try { + parsed = JSON.parse(body); + } catch { + parsed = { raw: body }; + } + return { + glCode: spec.glCode, + status: 'created', + id: parsed.resourceId ?? parsed.entityId, + response: parsed, + }; +} + +const results = []; +for (const spec of ACCOUNTS) { + results.push(await ensureAccount(spec)); +} + +console.log(JSON.stringify({ dryRun, tenant, base, results }, null, 2)); + +const missing = results.filter((r) => r.status === 'would_create' || r.status === 'created' || r.status === 'exists'); +if (missing.length !== ACCOUNTS.length) { + process.exit(1); +} diff --git a/scripts/deployment/smoke-btc-settlement.mjs b/scripts/deployment/smoke-btc-settlement.mjs new file mode 100644 index 0000000..d54878d --- /dev/null +++ b/scripts/deployment/smoke-btc-settlement.mjs @@ -0,0 +1,58 @@ +#!/usr/bin/env node +/** + * Smoke local settlement BTC rails after rebuild. + * Usage: node scripts/deployment/smoke-btc-settlement.mjs + * Env: SETTLEMENT_URL (default http://127.0.0.1:3011/api/v1/settlement) + * OMNL_API_KEY or SETTLEMENT_API_KEY + */ +const base = (process.env.SETTLEMENT_URL || 'http://127.0.0.1:3011/api/v1/settlement').replace( + /\/$/, + '', +); +const key = process.env.OMNL_API_KEY || process.env.SETTLEMENT_API_KEY || ''; + +async function get(path) { + const res = await fetch(`${base}${path}`, { + headers: { Authorization: `Bearer ${key}`, Accept: 'application/json' }, + }); + const text = await res.text(); + let body; + try { + body = JSON.parse(text); + } catch { + body = text; + } + return { status: res.status, body }; +} + +const health = await get('/health'); +const ledger = await get('/btc/ledger'); +const settlements = await get('/btc/settlements?limit=10'); + +const settledSats = ledger.body?.mintQueue?.settledSats ?? 0; +const ok = + health.status === 200 && + ledger.status === 200 && + settlements.status === 200 && + Number(settledSats) >= 100_000_000_000; + +console.log( + JSON.stringify( + { + base, + health: health.status, + ledger: ledger.status, + settlements: settlements.status, + custodyHo12015: ledger.body?.gl?.custodyHo12015, + settledSats, + settlementCount: ledger.body?.mintQueue?.settlementCount, + hot1000Settled: Number(settledSats) >= 100_000_000_000, + ok, + prodHint: + 'After deploy, set SETTLEMENT_URL=https://secure.omdnl.org/settlement and re-run this smoke', + }, + null, + 2, + ), +); +process.exit(ok ? 0 : 1); diff --git a/scripts/deployment/verify-cbtc-trade.mjs b/scripts/deployment/verify-cbtc-trade.mjs new file mode 100644 index 0000000..a3d5483 --- /dev/null +++ b/scripts/deployment/verify-cbtc-trade.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +/** + * Verify cBTC is tradable on the hot cUSDT PMM via classic DODO DVM pattern: + * transfer base → pool, then pool.sellBase(trader). + * + * Env: OMNL_MINT_OPERATOR_PRIVATE_KEY, RPC_URL_138, VERIFY_CBTC_SELL (default 0.0001) + */ +import { Contract, JsonRpcProvider, Wallet, parseUnits, formatUnits } from 'ethers'; + +const CBTC = '0xe94260c555ac1d9d3cc9e1632883452ebdf0082e'; +const POOL = process.env.CHAIN138_POOL_CBTC_CUSDT || '0x481CcE1046e1F1ab13BC37f0f374bb5ff75F3a8d'; +const pk = process.env.OMNL_MINT_OPERATOR_PRIVATE_KEY?.trim() || process.env.PRIVATE_KEY?.trim(); +if (!pk) { + console.error('OMNL_MINT_OPERATOR_PRIVATE_KEY required'); + process.exit(1); +} + +const provider = new JsonRpcProvider( + process.env.RPC_URL_138 || process.env.CHAIN_138_RPC_URL || 'https://rpc.d-bis.org', +); +const wallet = new Wallet(pk, provider); +const sellHuman = process.env.VERIFY_CBTC_SELL || '0.0001'; + +const erc = [ + 'function transfer(address,uint256) returns (bool)', + 'function balanceOf(address) view returns (uint256)', +]; +const poolAbi = [ + 'function _BASE_RESERVE_() view returns (uint256)', + 'function _QUOTE_RESERVE_() view returns (uint256)', + 'function querySellBase(address,uint256) view returns (uint256,uint256)', + 'function sellBase(address to) returns (uint256)', +]; + +const cbtc = new Contract(CBTC, erc, wallet); +const pool = new Contract(POOL, poolAbi, wallet); + +const base = await pool._BASE_RESERVE_(); +const quote = await pool._QUOTE_RESERVE_(); +const sellAmt = parseUnits(sellHuman, 8); +const bal = await cbtc.balanceOf(wallet.address); + +const out = { + pool: POOL, + reserves: { base: formatUnits(base, 8), quote: formatUnits(quote, 6) }, + hot: base >= parseUnits('1000', 8), + operatorBal: formatUnits(bal, 8), + sell: sellHuman, +}; + +if (bal < sellAmt) { + console.log(JSON.stringify({ ...out, ok: false, error: 'insufficient operator cBTC' }, null, 2)); + process.exit(1); +} + +const q = await pool.querySellBase(wallet.address, sellAmt); +out.quotedUsdt = formatUnits(q[0], 6); + +const t = await cbtc.transfer(POOL, sellAmt); +await t.wait(); +const tx = await pool.sellBase(wallet.address); +await tx.wait(); + +console.log(JSON.stringify({ ...out, ok: true, transferTx: t.hash, sellTx: tx.hash }, null, 2)); diff --git a/services/btc-intake/Dockerfile b/services/btc-intake/Dockerfile new file mode 100644 index 0000000..fd7f0e2 --- /dev/null +++ b/services/btc-intake/Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +COPY tsconfig.json ./ +RUN npm ci +COPY src ./src +RUN npm run build + +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --omit=dev +COPY --from=builder /app/dist ./dist +RUN mkdir -p /app/data && addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 \ + && chown -R nodejs:nodejs /app/data +USER nodejs +EXPOSE 3013 +ENV BTC_INTAKE_PORT=3013 +ENV BTC_INTAKE_DATA_DIR=/app/data +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3013/health',r=>process.exit(r.statusCode===200||r.statusCode===503?0:1))" +CMD ["node", "dist/server.js"] diff --git a/services/btc-intake/package-lock.json b/services/btc-intake/package-lock.json new file mode 100644 index 0000000..e8f17b2 --- /dev/null +++ b/services/btc-intake/package-lock.json @@ -0,0 +1,3112 @@ +{ + "name": "btc-intake-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "btc-intake-service", + "version": "1.0.0", + "dependencies": { + "axios": "^1.15.2", + "express": "^5.1.0" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "@types/node": "^20.19.33", + "ts-node": "^10.9.2", + "typescript": "^5.9.3", + "vitest": "^1.6.1" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.2.tgz", + "integrity": "sha512-d3KvEXBSo/lOAMc2u6fkyDHBvetBHeqD7wm/AcXfLpSOQwlmG9D/aQ0SFswVjv05p7ullQS7Mjohj6/VdbZuTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/services/btc-intake/package.json b/services/btc-intake/package.json index 539bace..2ca0fae 100644 --- a/services/btc-intake/package.json +++ b/services/btc-intake/package.json @@ -1,16 +1,23 @@ { "name": "btc-intake-service", "version": "1.0.0", - "description": "BTC intake watcher for Chain 138 cBTC mint workflows", + "description": "BTC intake watcher for Chain 138 cBTC mint workflows — bitcoind custody + settlement-middleware sink", "main": "dist/index.js", "scripts": { "build": "tsc", + "start": "node dist/server.js", + "dev": "ts-node src/server.ts", "test": "vitest run", "test:watch": "vitest" }, - "dependencies": {}, + "dependencies": { + "axios": "^1.15.2", + "express": "^5.1.0" + }, "devDependencies": { + "@types/express": "^5.0.6", "@types/node": "^20.19.33", + "ts-node": "^10.9.2", "typescript": "^5.9.3", "vitest": "^1.6.1" } diff --git a/services/btc-intake/src/adapters/bitcoind-custody.test.ts b/services/btc-intake/src/adapters/bitcoind-custody.test.ts new file mode 100644 index 0000000..b71c7f0 --- /dev/null +++ b/services/btc-intake/src/adapters/bitcoind-custody.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import axios from 'axios'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { BitcoindCustodyAdapter } from './bitcoind-custody'; + +vi.mock('axios'); + +describe('BitcoindCustodyAdapter', () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'btc-intake-')); + const posted: Array<{ method: string; params: unknown[] }> = []; + + beforeEach(() => { + posted.length = 0; + process.env.BTC_INTAKE_DATA_DIR = tmp; + process.env.BITCOIND_RPC_URL = 'http://127.0.0.1:18332'; + process.env.BITCOIND_RPC_USER = 'u'; + process.env.BITCOIND_RPC_PASS = 'p'; + process.env.BTC_CONFIRMATIONS_REQUIRED = '6'; + + vi.mocked(axios.create).mockReturnValue({ + post: vi.fn(async (_url: string, body: { method: string; params: unknown[] }) => { + posted.push({ method: body.method, params: body.params || [] }); + if (body.method === 'getnewaddress') { + return { data: { result: 'bcrt1qtestaddress0000000000000001' } }; + } + if (body.method === 'listtransactions') { + return { + data: { + result: [ + { + category: 'receive', + address: 'bcrt1qtestaddress0000000000000001', + txid: 'abc123', + amount: 0.01, + confirmations: 6, + label: 'omnl-inst-1', + time: 1_700_000_000, + }, + ], + }, + }; + } + if (body.method === 'getbalance') { + return { data: { result: 0.01 } }; + } + if (body.method === 'getblockchaininfo') { + return { data: { result: { chain: 'regtest' } } }; + } + return { data: { result: null } }; + }), + } as never); + }); + + afterEach(() => { + vi.restoreAllMocks(); + fs.rmSync(tmp, { recursive: true, force: true }); + }); + + it('allocates bech32 deposit address and maps instruction', async () => { + const adapter = new BitcoindCustodyAdapter(); + const address = await adapter.allocateDepositAddress({ + instructionId: 'inst-1', + clientId: 'client-1', + }); + expect(address).toBe('bcrt1qtestaddress0000000000000001'); + expect(posted.some((p) => p.method === 'getnewaddress')).toBe(true); + const mapPath = path.join(tmp, 'address-map.json'); + expect(fs.existsSync(mapPath)).toBe(true); + }); + + it('lists deposit events for mapped addresses', async () => { + const adapter = new BitcoindCustodyAdapter(); + await adapter.allocateDepositAddress({ + instructionId: 'inst-1', + clientId: 'client-1', + }); + const { events } = await adapter.listDepositEvents(); + expect(events.length).toBe(1); + expect(events[0].txId).toBe('abc123'); + expect(events[0].amountSats).toBe(1_000_000); + expect(events[0].confirmations).toBe(6); + expect(events[0].instructionId).toBe('inst-1'); + }); + + it('reports confirmed reserve balance in sats', async () => { + const adapter = new BitcoindCustodyAdapter(); + const sats = await adapter.getConfirmedReserveBalanceSats(); + expect(sats).toBe(1_000_000); + }); +}); diff --git a/services/btc-intake/src/adapters/bitcoind-custody.ts b/services/btc-intake/src/adapters/bitcoind-custody.ts new file mode 100644 index 0000000..ec78310 --- /dev/null +++ b/services/btc-intake/src/adapters/bitcoind-custody.ts @@ -0,0 +1,163 @@ +import axios, { type AxiosInstance } from 'axios'; +import fs from 'fs'; +import path from 'path'; +import type { CustodyAdapter } from '../custody-adapter'; +import type { BitcoinDepositEvent } from '../types'; + +type AddressMap = Record; // address → instructionId + +type BitcoindTx = { + address?: string; + category?: string; + amount?: number; + confirmations?: number; + txid?: string; + label?: string; + time?: number; +}; + +function dataDir(): string { + const dir = + process.env.BTC_INTAKE_DATA_DIR || + path.resolve(process.cwd(), 'data/btc-intake'); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + return dir; +} + +function addressMapPath(): string { + return path.join(dataDir(), 'address-map.json'); +} + +function loadAddressMap(): AddressMap { + const p = addressMapPath(); + if (!fs.existsSync(p)) return {}; + try { + return JSON.parse(fs.readFileSync(p, 'utf8')) as AddressMap; + } catch { + return {}; + } +} + +function saveAddressMap(map: AddressMap): void { + fs.writeFileSync(addressMapPath(), JSON.stringify(map, null, 2)); +} + +function btcToSats(amountBtc: number): number { + return Math.round(amountBtc * 1e8); +} + +export class BitcoindCustodyAdapter implements CustodyAdapter { + private readonly client: AxiosInstance; + private addressMap: AddressMap; + private processedTxKeys = new Set(); + + constructor(opts?: { + rpcUrl?: string; + rpcUser?: string; + rpcPass?: string; + }) { + const rpcUrl = (opts?.rpcUrl || process.env.BITCOIND_RPC_URL || '').replace(/\/$/, ''); + const rpcUser = opts?.rpcUser || process.env.BITCOIND_RPC_USER || ''; + const rpcPass = opts?.rpcPass || process.env.BITCOIND_RPC_PASS || ''; + if (!rpcUrl) { + throw new Error('BITCOIND_RPC_URL required'); + } + this.client = axios.create({ + baseURL: rpcUrl, + timeout: 60000, + auth: rpcUser ? { username: rpcUser, password: rpcPass } : undefined, + headers: { 'Content-Type': 'application/json' }, + }); + this.addressMap = loadAddressMap(); + } + + private async rpc(method: string, params: unknown[] = []): Promise { + const { data } = await this.client.post('', { + jsonrpc: '1.0', + id: `btc-intake-${Date.now()}`, + method, + params, + }); + if (data?.error) { + throw new Error(`bitcoind RPC ${method}: ${JSON.stringify(data.error)}`); + } + return data.result as T; + } + + async ping(): Promise { + try { + await this.rpc('getblockchaininfo'); + return true; + } catch { + return false; + } + } + + async allocateDepositAddress(input: { + instructionId: string; + clientId: string; + }): Promise { + const label = `omnl-${input.instructionId}`; + const address = await this.rpc('getnewaddress', [label, 'bech32']); + this.addressMap[address] = input.instructionId; + this.addressMap[address.toLowerCase()] = input.instructionId; + saveAddressMap(this.addressMap); + return address; + } + + async listDepositEvents( + cursor?: string, + ): Promise<{ events: BitcoinDepositEvent[]; cursor?: string }> { + const skip = cursor ? Number(cursor) || 0 : 0; + const count = 100; + const txs = await this.rpc('listtransactions', ['*', count, skip, true]); + const events: BitcoinDepositEvent[] = []; + const nextCursor = String(skip + txs.length); + + for (const tx of txs) { + if (tx.category !== 'receive' || !tx.txid || !tx.address) continue; + const instructionId = + this.addressMap[tx.address] || + this.addressMap[tx.address.toLowerCase()] || + (tx.label?.startsWith('omnl-') ? tx.label.slice(5) : undefined); + if (!instructionId) continue; + + const key = `${tx.txid}:${tx.address}:${instructionId}`; + if (this.processedTxKeys.has(key) && (tx.confirmations ?? 0) >= 6) { + // Still emit updates for confirmation progression on first passes; + // allow re-emit when confirmations change by not permanently skipping early. + } + this.processedTxKeys.add(key); + + events.push({ + instructionId, + txId: tx.txid, + confirmations: Math.max(0, tx.confirmations ?? 0), + amountSats: btcToSats(Number(tx.amount) || 0), + type: (tx.confirmations ?? 0) < 0 ? 'reorg' : 'deposit', + observedAt: tx.time + ? new Date(tx.time * 1000).toISOString() + : new Date().toISOString(), + }); + } + + return { events, cursor: nextCursor }; + } + + async getConfirmedReserveBalanceSats(): Promise { + const minConf = Number(process.env.BTC_CONFIRMATIONS_REQUIRED || 6); + const reserveCsv = (process.env.BTC_RESERVE_ADDRESSES || '').trim(); + if (reserveCsv) { + const addresses = reserveCsv.split(',').map((a) => a.trim()).filter(Boolean); + let total = 0; + for (const address of addresses) { + const received = await this.rpc('getreceivedbyaddress', [address, minConf]); + total += btcToSats(Number(received) || 0); + } + return total; + } + // Fall back to wallet confirmed balance + const bal = await this.rpc('getbalance', ['*', minConf]); + return btcToSats(Number(bal) || 0); + } +} diff --git a/services/btc-intake/src/adapters/settlement-mint-sink.ts b/services/btc-intake/src/adapters/settlement-mint-sink.ts new file mode 100644 index 0000000..10291b5 --- /dev/null +++ b/services/btc-intake/src/adapters/settlement-mint-sink.ts @@ -0,0 +1,92 @@ +import axios from 'axios'; +import fs from 'fs'; +import path from 'path'; +import type { MintJobSink } from '../custody-adapter'; +import type { MintJob } from '../types'; + +type JobRecord = MintJob & { + settlementId?: string; + settlementPhase?: string; + lastError?: string; + updatedAt: string; +}; + +function dataDir(): string { + const dir = + process.env.BTC_INTAKE_DATA_DIR || + path.resolve(process.cwd(), 'data/btc-intake'); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + return dir; +} + +function jobPath(id: string): string { + return path.join(dataDir(), `mint-job-${id}.json`); +} + +function saveJob(job: JobRecord): void { + fs.writeFileSync(jobPath(job.id), JSON.stringify(job, null, 2)); +} + +export class SettlementMintJobSink implements MintJobSink { + private readonly settlementBase: string; + private readonly apiKey: string; + + constructor(opts?: { settlementUrl?: string; apiKey?: string }) { + this.settlementBase = ( + opts?.settlementUrl || + process.env.SETTLEMENT_MIDDLEWARE_URL || + 'http://localhost:3011' + ).replace(/\/$/, ''); + this.apiKey = opts?.apiKey || process.env.OMNL_API_KEY || ''; + } + + async enqueue(job: MintJob): Promise { + const record: JobRecord = { + ...job, + updatedAt: new Date().toISOString(), + }; + saveJob(record); + + if (!job.txId || !job.depositAddress) { + record.lastError = 'MintJob missing txId or depositAddress — cannot settle'; + saveJob(record); + throw new Error(record.lastError); + } + + const headers: Record = { 'Content-Type': 'application/json' }; + if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`; + + const body = { + idempotencyKey: `btc-l1-${job.txId}-${job.instructionId}`, + instructionId: job.instructionId, + txId: job.txId, + depositAddress: job.depositAddress, + amountSats: job.amountSats, + confirmations: job.confirmations ?? Number(process.env.BTC_CONFIRMATIONS_REQUIRED || 6), + recipientAddress: job.chain138VaultAddress, + }; + + try { + const { data } = await axios.post( + `${this.settlementBase}/api/v1/settlement/btc/l1-settle`, + body, + { headers, timeout: 180000 }, + ); + record.settlementId = data.settlementId; + record.settlementPhase = data.phase; + record.updatedAt = new Date().toISOString(); + if (data.phase === 'FAILED') { + record.lastError = Array.isArray(data.errors) ? data.errors.join('; ') : 'settlement FAILED'; + } + saveJob(record); + if (data.phase === 'FAILED') { + throw new Error(record.lastError || 'btc l1 settle failed'); + } + } catch (err) { + record.lastError = err instanceof Error ? err.message : String(err); + record.updatedAt = new Date().toISOString(); + saveJob(record); + throw err; + } + } +} diff --git a/services/btc-intake/src/custody-adapter.ts b/services/btc-intake/src/custody-adapter.ts index 7caa30c..78b6109 100644 --- a/services/btc-intake/src/custody-adapter.ts +++ b/services/btc-intake/src/custody-adapter.ts @@ -16,6 +16,9 @@ export interface MintJobSink { amountSats: number; status: 'queued'; createdAt: string; + txId?: string; + depositAddress?: string; + confirmations?: number; }): Promise; } diff --git a/services/btc-intake/src/index.ts b/services/btc-intake/src/index.ts index b288eb3..9115f0b 100644 --- a/services/btc-intake/src/index.ts +++ b/services/btc-intake/src/index.ts @@ -1,3 +1,5 @@ export * from './types'; export * from './custody-adapter'; export * from './native-bitcoin-watcher'; +export { BitcoindCustodyAdapter } from './adapters/bitcoind-custody'; +export { SettlementMintJobSink } from './adapters/settlement-mint-sink'; diff --git a/services/btc-intake/src/native-bitcoin-watcher.test.ts b/services/btc-intake/src/native-bitcoin-watcher.test.ts index 36a53a3..5f221b1 100644 --- a/services/btc-intake/src/native-bitcoin-watcher.test.ts +++ b/services/btc-intake/src/native-bitcoin-watcher.test.ts @@ -118,6 +118,9 @@ describe('NativeBitcoinWatcher', () => { instructionId: instruction.id, amountSats: 250_000_000, canonicalSymbol: 'cBTC', + txId: 'tx-1', + depositAddress: instruction.depositAddress, + confirmations: 6, }); }); diff --git a/services/btc-intake/src/native-bitcoin-watcher.ts b/services/btc-intake/src/native-bitcoin-watcher.ts index a536742..3e9d497 100644 --- a/services/btc-intake/src/native-bitcoin-watcher.ts +++ b/services/btc-intake/src/native-bitcoin-watcher.ts @@ -203,6 +203,9 @@ export class NativeBitcoinWatcher { amountSats, status: 'queued', createdAt: timestamp, + txId: instruction.observedTxId, + depositAddress: instruction.depositAddress, + confirmations: instruction.currentConfirmations, }; await this.mintSink.enqueue(mintJob); diff --git a/services/btc-intake/src/server.ts b/services/btc-intake/src/server.ts new file mode 100644 index 0000000..dbcab5b --- /dev/null +++ b/services/btc-intake/src/server.ts @@ -0,0 +1,142 @@ +import express from 'express'; +import type { AuditPublisher, OutstandingPolicy } from './custody-adapter'; +import { BitcoindCustodyAdapter } from './adapters/bitcoind-custody'; +import { SettlementMintJobSink } from './adapters/settlement-mint-sink'; +import { NativeBitcoinWatcher } from './native-bitcoin-watcher'; +import type { AuditRecord, BasketMandateSnapshot } from './types'; + +const PORT = Number(process.env.BTC_INTAKE_PORT || 3013); +const SYNC_INTERVAL_MS = Number(process.env.BTC_INTAKE_SYNC_INTERVAL_MS || 30_000); + +class ConsoleAuditPublisher implements AuditPublisher { + async publish(record: AuditRecord): Promise { + // eslint-disable-next-line no-console + console.log( + JSON.stringify({ + level: 'audit', + service: 'btc-intake', + ...record, + }), + ); + } +} + +class EnvOutstandingPolicy implements OutstandingPolicy { + async getCurrentOutstandingSats(): Promise { + return Number(process.env.BTC_CURRENT_OUTSTANDING_SATS || 0); + } + + async getMaxOutstandingSats(): Promise { + return Number(process.env.BTC_MAX_OUTSTANDING_SATS || 0); + } +} + +async function main(): Promise { + const adapter = new BitcoindCustodyAdapter(); + const mintSink = new SettlementMintJobSink(); + const audit = new ConsoleAuditPublisher(); + const outstanding = new EnvOutstandingPolicy(); + const watcher = new NativeBitcoinWatcher(adapter, mintSink, audit, outstanding); + + let lastSyncAt: string | null = null; + let lastSyncError: string | null = null; + let syncing = false; + + const runSync = async () => { + if (syncing) return; + syncing = true; + try { + await watcher.sync(); + lastSyncAt = new Date().toISOString(); + lastSyncError = null; + } catch (err) { + lastSyncError = err instanceof Error ? err.message : String(err); + // eslint-disable-next-line no-console + console.error('[btc-intake] sync error', lastSyncError); + } finally { + syncing = false; + } + }; + + const app = express(); + app.use(express.json({ limit: '1mb' })); + + app.get('/health', async (_req, res) => { + const bitcoindOk = await adapter.ping(); + res.status(bitcoindOk ? 200 : 503).json({ + service: 'btc-intake', + status: bitcoindOk ? 'ok' : 'degraded', + bitcoind: bitcoindOk, + lastSyncAt, + lastSyncError, + syncing, + network: process.env.BTC_NETWORK || 'mainnet', + }); + }); + + app.post('/deposit-instructions', async (req, res) => { + try { + const basketMandate = req.body?.basketMandate as BasketMandateSnapshot | undefined; + if ( + !basketMandate?.id || + !basketMandate.clientId || + !basketMandate.chain138VaultAddress?.startsWith('0x') + ) { + res.status(400).json({ + error: 'basketMandate with id, clientId, chain138VaultAddress (0x) required', + }); + return; + } + const expectedAmountSats = + req.body?.expectedAmountSats != null + ? Number(req.body.expectedAmountSats) + : undefined; + const instruction = await watcher.createDepositInstruction({ + basketMandate, + expectedAmountSats, + }); + res.status(201).json(instruction); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + app.get('/deposit-instructions/:id', (req, res) => { + const instruction = watcher.getInstruction(req.params.id); + if (!instruction) { + res.status(404).json({ error: 'not found' }); + return; + } + res.json(instruction); + }); + + app.post('/sync', async (_req, res) => { + await runSync(); + res.json({ + ok: !lastSyncError, + lastSyncAt, + lastSyncError, + mintJobs: watcher.getMintJobs(), + }); + }); + + app.get('/mint-jobs', (_req, res) => { + res.json({ jobs: watcher.getMintJobs() }); + }); + + setInterval(() => { + void runSync(); + }, SYNC_INTERVAL_MS); + void runSync(); + + app.listen(PORT, () => { + // eslint-disable-next-line no-console + console.log(`[btc-intake] listening on :${PORT} (sync every ${SYNC_INTERVAL_MS}ms)`); + }); +} + +main().catch((err) => { + // eslint-disable-next-line no-console + console.error('[btc-intake] fatal', err); + process.exit(1); +}); diff --git a/services/btc-intake/src/types.ts b/services/btc-intake/src/types.ts index add87c2..607985f 100644 --- a/services/btc-intake/src/types.ts +++ b/services/btc-intake/src/types.ts @@ -47,6 +47,10 @@ export interface MintJob { amountSats: number; status: 'queued'; createdAt: string; + /** L1 tx id when known at enqueue time */ + txId?: string; + depositAddress?: string; + confirmations?: number; } export interface AuditRecord { diff --git a/services/settlement-middleware/dist/adapters/fineract.js b/services/settlement-middleware/dist/adapters/fineract.js index c74d624..e6236f6 100644 --- a/services/settlement-middleware/dist/adapters/fineract.js +++ b/services/settlement-middleware/dist/adapters/fineract.js @@ -6,10 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.fineractOfficeReachable = fineractOfficeReachable; exports.postJournalEntry = postJournalEntry; exports.resolveGlAccountId = resolveGlAccountId; +exports.resolveBtcGlAccountIds = resolveBtcGlAccountIds; exports.fetchGlBalances = fetchGlBalances; const axios_1 = __importDefault(require("axios")); const settlement_store_1 = require("../store/settlement-store"); -const GL_HINTS = ['1000', '1050', '1410', '2000', '2100', '2200', '2300']; +const GL_HINTS = ['1000', '1050', '12015', '12424', '1410', '2000', '2100', '2200', '2300']; function fineractBase() { return (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, ''); } @@ -144,7 +145,13 @@ async function fetchGlBalancesFromJournals(officeId) { return out; } async function postJournalEntry(entry) { - const { data } = await axios_1.default.post(`${fineractBase()}/journalentries`, entry, { + const payload = { + ...entry, + currencyCode: 'USD', + dateFormat: 'yyyy-MM-dd', + locale: 'en', + }; + const { data } = await axios_1.default.post(`${fineractBase()}/journalentries`, payload, { headers: authHeaders(), timeout: 60000, }); @@ -170,6 +177,16 @@ async function resolveGlAccountId(glCode) { glCodeCache.set(code, match.id); return match.id; } +/** Resolve Fineract GL account ids for L1 BTC custody legs (HO 12015 / Office 24 12424). */ +async function resolveBtcGlAccountIds(_officeId) { + const custodyHoGlCode = '12015'; + const interofficeBtcGlCode = '12424'; + const [custodyHoGlId, interofficeBtcGlId] = await Promise.all([ + resolveGlAccountId(custodyHoGlCode), + resolveGlAccountId(interofficeBtcGlCode), + ]); + return { custodyHoGlId, interofficeBtcGlId, custodyHoGlCode, interofficeBtcGlCode }; +} async function fetchGlBalances(officeId) { const base = fineractBase(); if (!base || !process.env.OMNL_FINERACT_PASSWORD) diff --git a/services/settlement-middleware/dist/adapters/omnl.js b/services/settlement-middleware/dist/adapters/omnl.js index 6ae446e..c960542 100644 --- a/services/settlement-middleware/dist/adapters/omnl.js +++ b/services/settlement-middleware/dist/adapters/omnl.js @@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.archiveIso20022 = archiveIso20022; exports.requestTokenMint = requestTokenMint; exports.requestTokenTransfer = requestTokenTransfer; +exports.requestFiatLpToLiquidity = requestFiatLpToLiquidity; const axios_1 = __importDefault(require("axios")); async function archiveIso20022(payload) { const base = (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, ''); @@ -65,3 +66,16 @@ async function requestTokenTransfer(payload) { throw err; } } +async function requestFiatLpToLiquidity(payload) { + const base = (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, ''); + const key = process.env.OMNL_API_KEY || ''; + const headers = { 'Content-Type': 'application/json' }; + if (key) + headers.Authorization = `Bearer ${key}`; + const { data } = await axios_1.default.post(`${base}/api/v1/omnl/settlement/fiat-lp-to-liquidity`, { ...payload, dryRun: payload.dryRun }, { headers, timeout: 300000 }); + return { + swaps: data.swaps ?? [], + positions: data.positions, + dryRun: Boolean(data.dryRun ?? payload.dryRun), + }; +} diff --git a/services/settlement-middleware/dist/adapters/pricing.js b/services/settlement-middleware/dist/adapters/pricing.js new file mode 100644 index 0000000..3744637 --- /dev/null +++ b/services/settlement-middleware/dist/adapters/pricing.js @@ -0,0 +1,68 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.resolveLiveBtcUsd = resolveLiveBtcUsd; +const axios_1 = __importDefault(require("axios")); +const CBTC_ADDRESS = '0xe94260c555ac1d9d3cc9e1632883452ebdf0082e'; +const CHAIN_138 = 138; +function tokenAggregationBase() { + return (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, ''); +} +/** + * Resolve live BTC/USD for settlement. Rejects unresolved/stale prices — + * production must not fall back to the $90k repo snapshot. + */ +async function resolveLiveBtcUsd() { + const base = tokenAggregationBase(); + const key = process.env.OMNL_API_KEY || ''; + const headers = { 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`; + let data; + try { + const res = await axios_1.default.get(url, { headers, timeout: 30000 }); + data = res.data; + } + catch (err) { + if (axios_1.default.isAxiosError(err)) { + const status = err.response?.status; + const body = err.response?.data; + const detail = typeof body === 'string' + ? body.slice(0, 400) + : body != null + ? JSON.stringify(body).slice(0, 400) + : err.message; + throw new Error(`Live BTC/USD fetch failed${status != null ? ` HTTP ${status}` : ''}: ${detail}`); + } + throw err; + } + const map = (data ?? {}); + const row = map[CBTC_ADDRESS.toLowerCase()] ?? map[CBTC_ADDRESS]; + 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.'); + } + // Guard against known static repo fallback used in non-production + const requireLive = process.env.OMNL_REQUIRE_LIVE_BTC_PRICE === '1' || process.env.NODE_ENV === 'production'; + if (requireLive && Math.abs(rate - 90000) < 0.01) { + // Allow only if explicitly overridden via env canonical + const envOverride = Number(process.env.CHAIN138_CANONICAL_PRICE_USD_BTC || + process.env.CANONICAL_PRICE_USD_BTC || + process.env.BTC_PRICE_USD || + ''); + if (!(envOverride > 0 && Math.abs(envOverride - 90000) < 0.01)) { + throw new Error('BTC/USD appears to be repo-fallback ($90000) — live oracle required for production settlement'); + } + } + return { + btcUsdRate: rate, + source: 'token-aggregation/metamask-spot-prices', + asOf: new Date().toISOString(), + tokenAddress: CBTC_ADDRESS, + }; +} diff --git a/services/settlement-middleware/dist/adapters/swift.js b/services/settlement-middleware/dist/adapters/swift.js index ef2f9ff..70b0fe5 100644 --- a/services/settlement-middleware/dist/adapters/swift.js +++ b/services/settlement-middleware/dist/adapters/swift.js @@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.forwardSwiftToListener = forwardSwiftToListener; exports.buildMt103Stub = buildMt103Stub; exports.buildMt102Stub = buildMt102Stub; +exports.buildMt202Stub = buildMt202Stub; +exports.buildMt910Stub = buildMt910Stub; const axios_1 = __importDefault(require("axios")); async function forwardSwiftToListener(raw) { const url = (process.env.SWIFT_LISTENER_URL || 'http://192.168.11.114:8788').replace(/\/$/, ''); @@ -40,3 +42,25 @@ ${fields.beneficiary} :70:${fields.remittance} -}`; } +/** MT202 — interbank cover / institution transfer (SBLC correspondent leg). */ +function buildMt202Stub(fields) { + const ord = fields.orderingInstitution ? `:52A:${fields.orderingInstitution}\n` : ''; + const acct = fields.accountWithInstitution ? `:57A:${fields.accountWithInstitution}\n` : ''; + const ben = fields.beneficiaryInstitution ? `:58A:${fields.beneficiaryInstitution}\n` : ''; + return `{1:F01OMNLUS33XXXX0000000000}{2:I202BANKUS33XXXXN}{4: +:20:${fields.senderRef} +:21:${fields.relatedRef} +:32A:${fields.valueDate.replace(/-/g, '').slice(2)}${fields.currency}${fields.amount.replace('.', ',')} +${ord}${acct}${ben}:70:${fields.remittance} +-}`; +} +/** MT910 — confirmation of credit (advice to beneficiary bank). */ +function buildMt910Stub(fields) { + const acct = fields.accountWithInstitution ? `:57A:${fields.accountWithInstitution}\n` : ''; + return `{1:F01OMNLUS33XXXX0000000000}{2:I910BANKUS33XXXXN}{4: +:20:${fields.transactionRef} +:21:${fields.relatedRef} +:32A:${fields.valueDate.replace(/-/g, '').slice(2)}${fields.currency}${fields.amount.replace('.', ',')} +${acct}:72:${fields.remittance} +-}`; +} diff --git a/services/settlement-middleware/dist/api/routes/settlement.js b/services/settlement-middleware/dist/api/routes/settlement.js index 6ae4913..80e6b83 100644 --- a/services/settlement-middleware/dist/api/routes/settlement.js +++ b/services/settlement-middleware/dist/api/routes/settlement.js @@ -11,7 +11,10 @@ const office_scope_1 = require("../../workflows/office-scope"); const settlement_store_1 = require("../../store/settlement-store"); const swift_inbound_1 = require("../../workflows/swift-inbound"); const z_bank_m0_m4_1 = require("../../workflows/z-bank-m0-m4"); +const fiat_lp_to_liquidity_1 = require("../../workflows/fiat-lp-to-liquidity"); +const btc_l1_settlement_1 = require("../../workflows/btc-l1-settlement"); const fineract_1 = require("../../adapters/fineract"); +const pricing_1 = require("../../adapters/pricing"); const auth_1 = require("../../middleware/auth"); async function respondPublicMoneySupply(res, officeId) { const cfg = (0, config_1.loadConfig)(); @@ -273,6 +276,29 @@ function createSettlementRouter() { res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); } }); + router.post('/convert/fiat-lp-to-liquidity', async (req, res) => { + if (!(0, auth_1.requireApiKey)(req, res)) + return; + try { + const body = req.body; + if (!body.idempotencyKey || !body.holderAddress?.startsWith('0x')) { + res.status(400).json({ error: 'idempotencyKey and holderAddress (0x) required' }); + return; + } + if (!body.convertAll && !body.symbol) { + res.status(400).json({ error: 'symbol required unless convertAll is true' }); + return; + } + const record = await (0, fiat_lp_to_liquidity_1.processFiatLpToLiquidity)({ + ...body, + officeId: (0, config_1.settlementOfficeId)(body.officeId), + }); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } + catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); router.post('/transfer/internal', async (req, res) => { if (!(0, auth_1.requireApiKey)(req, res)) return; @@ -353,5 +379,118 @@ function createSettlementRouter() { res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); } }); + router.post('/btc/l1-settle', async (req, res) => { + if (!(0, auth_1.requireApiKey)(req, res)) + return; + try { + const body = req.body; + if (!body.idempotencyKey || + !body.instructionId || + !body.txId || + !body.depositAddress || + body.amountSats == null || + !body.recipientAddress) { + res.status(400).json({ + error: 'idempotencyKey, instructionId, txId, depositAddress, amountSats, recipientAddress required', + }); + return; + } + const record = await (0, btc_l1_settlement_1.processBtcL1Settlement)({ + ...body, + officeId: (0, config_1.settlementOfficeId)(body.officeId), + confirmations: Number(body.confirmations ?? 0), + amountSats: Number(body.amountSats), + }); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } + catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + /** + * Mark FAILED/partial BTC L1 settle as SETTLED when books + mint already done. + * Does not re-journal or remint. + */ + router.post('/btc/l1-reconcile', (req, res) => { + if (!(0, auth_1.requireApiKey)(req, res)) + return; + try { + const body = req.body; + if ((!body.settlementId && !body.idempotencyKey) || + body.amountSats == null || + !body.txId || + !body.depositAddress || + !body.instructionId || + !body.recipientAddress || + !body.chainTxHash || + body.btcUsdRate == null) { + res.status(400).json({ + error: 'settlementId|idempotencyKey, amountSats, txId, depositAddress, instructionId, recipientAddress, chainTxHash, btcUsdRate required', + }); + return; + } + const record = (0, btc_l1_settlement_1.reconcileBtcL1Settlement)({ + ...body, + amountSats: Number(body.amountSats), + btcUsdRate: Number(body.btcUsdRate), + }); + res.status(200).json(record); + } + catch (e) { + res.status(400).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.get('/btc/settlements', (req, res) => { + if (!(0, auth_1.requireApiKey)(req, res)) + return; + const limit = Math.min(Number(req.query.limit) || 50, 500); + const rows = settlement_store_1.settlementStore + .list(limit * 4) + .filter((r) => r.btcSettlement != null) + .slice(0, limit); + res.json({ count: rows.length, settlements: rows }); + }); + router.get('/btc/ledger', async (_req, res) => { + if (!(0, auth_1.requireApiKey)(_req, res)) + return; + try { + const [balancesHo, balancesO24, quote] = await Promise.all([ + (0, fineract_1.fetchGlBalances)(1), + (0, fineract_1.fetchGlBalances)(officeId), + (0, pricing_1.resolveLiveBtcUsd)().catch((e) => ({ + error: e instanceof Error ? e.message : String(e), + })), + ]); + const btcSettlements = settlement_store_1.settlementStore + .list(500) + .filter((r) => r.btcSettlement != null); + const outstandingSats = btcSettlements + .filter((r) => r.phase === 'CHAIN_MINT_REQUESTED' || r.phase === 'FINERACT_POSTED') + .reduce((sum, r) => sum + (r.btcSettlement?.amountSats ?? 0), 0); + const settledSats = btcSettlements + .filter((r) => r.phase === 'SETTLED') + .reduce((sum, r) => sum + (r.btcSettlement?.amountSats ?? 0), 0); + res.json({ + asOf: new Date().toISOString(), + gl: { + custodyHo12015: balancesHo['12015'] ?? '0', + interofficeBtc12424: balancesHo['12424'] ?? balancesO24['12424'] ?? '0', + m2Broad2200: balancesO24['2200'] ?? '0', + m3Token2300: balancesO24['2300'] ?? '0', + }, + btcUsd: 'btcUsdRate' in quote + ? { rate: quote.btcUsdRate, source: quote.source, asOf: quote.asOf } + : { error: quote.error }, + mintQueue: { + outstandingSats, + settledSats, + settlementCount: btcSettlements.length, + }, + }); + } + catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); return router; } diff --git a/services/settlement-middleware/dist/store/settlement-store.js b/services/settlement-middleware/dist/store/settlement-store.js index 1c128d6..6ac44fe 100644 --- a/services/settlement-middleware/dist/store/settlement-store.js +++ b/services/settlement-middleware/dist/store/settlement-store.js @@ -39,8 +39,9 @@ class SettlementStore { return fs_1.default .readdirSync(dir) .filter((f) => f.endsWith('.json') && !f.startsWith('key-')) - .slice(0, limit) - .map((f) => JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, f), 'utf8'))); + .map((f) => JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, f), 'utf8'))) + .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt))) + .slice(0, limit); } } exports.SettlementStore = SettlementStore; diff --git a/services/settlement-middleware/dist/workflows/btc-l1-settlement.js b/services/settlement-middleware/dist/workflows/btc-l1-settlement.js new file mode 100644 index 0000000..d7b51d6 --- /dev/null +++ b/services/settlement-middleware/dist/workflows/btc-l1-settlement.js @@ -0,0 +1,278 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.allowChainMintExecute = allowChainMintExecute; +exports.formatSettlementError = formatSettlementError; +exports.processBtcL1Settlement = processBtcL1Settlement; +exports.reconcileBtcL1Settlement = reconcileBtcL1Settlement; +const uuid_1 = require("uuid"); +const axios_1 = __importDefault(require("axios")); +const settlement_core_1 = require("@dbis/settlement-core"); +const config_1 = require("../config"); +const fineract_1 = require("../adapters/fineract"); +const omnl_1 = require("../adapters/omnl"); +const pricing_1 = require("../adapters/pricing"); +const settlement_store_1 = require("../store/settlement-store"); +const MIN_CONFIRMATIONS = Number(process.env.BTC_CONFIRMATIONS_REQUIRED || 6); +/** Env SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE overrides production JSON when set. */ +function allowChainMintExecute() { + 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 (0, config_1.loadConfig)().production.allowChainMintExecute === true; +} +function formatSettlementError(err) { + if (axios_1.default.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. + */ +async function processBtcL1Settlement(input) { + const profile = (0, config_1.loadOfficeProfile)(); + const officeId = (0, config_1.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 = settlement_store_1.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 = settlement_store_1.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 = (0, settlement_core_1.satsToBtcDecimal)(req.amountSats); + let record = existing ?? { + settlementId: (0, uuid_1.v4)(), + 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, patch = {}) => { + record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch }; + settlement_store_1.settlementStore.save(record); + }; + try { + advance('VALIDATED'); + const quote = await (0, pricing_1.resolveLiveBtcUsd)(); + const usdValue = (0, settlement_core_1.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 = (0, settlement_core_1.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 (0, fineract_1.resolveBtcGlAccountIds)(officeId); + const custodyJe = await (0, fineract_1.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 (0, fineract_1.fetchGlBalances)(officeId); + const snap = (0, settlement_core_1.buildMoneySupplySnapshot)(officeId, balances); + const { loadAmount, fullyBacked } = (0, settlement_core_1.m2FiatToTokenLoadAmount)(usdValue, snap.M2.broadMoney); + const m3Check = (0, settlement_core_1.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([ + (0, fineract_1.resolveGlAccountId)(profile.settlement.glM2 ?? settlement_core_1.GL_CODES.M2_BROAD), + (0, fineract_1.resolveGlAccountId)(profile.settlement.glM3 ?? settlement_core_1.GL_CODES.M3_TOKEN_LIABILITY), + ]); + const m2Je = await (0, fineract_1.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 (0, omnl_1.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. + */ +function reconcileBtcL1Settlement(input) { + 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 + ? settlement_store_1.settlementStore.getById(input.settlementId) + : settlement_store_1.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 = (0, settlement_core_1.satsToBtcDecimal)(input.amountSats); + const usdValue = input.usdValue ?? (0, settlement_core_1.btcUsdToJournalAmount)(input.amountSats, input.btcUsdRate); + const officeId = (0, config_1.settlementOfficeId)(existing.request.officeId); + const record = { + ...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, + }, + }, + }; + settlement_store_1.settlementStore.save(record); + return record; +} diff --git a/services/settlement-middleware/dist/workflows/btc-l1-settlement.test.js b/services/settlement-middleware/dist/workflows/btc-l1-settlement.test.js new file mode 100644 index 0000000..4790bce --- /dev/null +++ b/services/settlement-middleware/dist/workflows/btc-l1-settlement.test.js @@ -0,0 +1,62 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +/** + * Unit tests for BTC L1 settlement helpers (pricing gate + amount math). + * Run via: npm run build && npm test (node --test) + */ +const node_test_1 = require("node:test"); +const strict_1 = __importDefault(require("node:assert/strict")); +const settlement_core_1 = require("@dbis/settlement-core"); +(0, node_test_1.describe)('btc-l1-settlement amounts', () => { + (0, node_test_1.it)('mints cBTC amount from sats at 8 decimals', () => { + strict_1.default.equal((0, settlement_core_1.satsToBtcDecimal)(25_000_000), '0.25000000'); + }); + (0, node_test_1.it)('posts USD fair value = sats × rate', () => { + // 0.5 BTC @ 92_500 = 46_250.00 + strict_1.default.equal((0, settlement_core_1.btcUsdToJournalAmount)(50_000_000, 92_500), '46250.00'); + }); +}); +(0, node_test_1.describe)('pricing gate reject static fallback', () => { + (0, node_test_1.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); + strict_1.default.equal(isSuspicious, true); + }); + (0, node_test_1.it)('allows non-fallback live rates', () => { + const rate = 97_432.55; + const requireLive = true; + const isSuspicious = requireLive && Math.abs(rate - 90000) < 0.01; + strict_1.default.equal(isSuspicious, false); + }); +}); +(0, node_test_1.describe)('idempotency key shape', () => { + (0, node_test_1.it)('derives stable key from txId + instructionId', () => { + const txId = 'deadbeef'; + const instructionId = 'inst-42'; + const key = `btc-l1-${txId}-${instructionId}`; + strict_1.default.equal(key, 'btc-l1-deadbeef-inst-42'); + }); +}); +(0, node_test_1.describe)('allowChainMintExecute env override', () => { + (0, node_test_1.it)('treats SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=0 as dry-run', () => { + const env = '0'; + const cfgAllow = true; + const allow = env === '0' || env === 'false' ? false : env === '1' || env === 'true' ? true : cfgAllow; + strict_1.default.equal(allow, false); + }); + (0, node_test_1.it)('treats SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=1 as execute', () => { + const env = '1'; + const cfgAllow = false; + const allow = env === '0' || env === 'false' ? false : env === '1' || env === 'true' ? true : cfgAllow; + strict_1.default.equal(allow, true); + }); +}); +// silence unused mock import in older node +void node_test_1.mock; diff --git a/services/settlement-middleware/dist/workflows/external-transfer.js b/services/settlement-middleware/dist/workflows/external-transfer.js index 937feba..6dbe3db 100644 --- a/services/settlement-middleware/dist/workflows/external-transfer.js +++ b/services/settlement-middleware/dist/workflows/external-transfer.js @@ -112,6 +112,29 @@ async function processExternalTransfer(input) { }); if (cfg.rails.swift.enabled) { await (0, swift_1.forwardSwiftToListener)(swiftRaw); + if (req.tradeFinance?.instrument === 'SBLC') { + const cover = (0, swift_1.buildMt202Stub)({ + senderRef: `${req.idempotencyKey}-COV`, + relatedRef: req.idempotencyKey, + currency: req.currency, + amount: req.amount, + valueDate: req.valueDate, + orderingInstitution: 'OMNLUS33', + beneficiaryInstitution: req.creditorBic ?? 'BANKUS33', + remittance: `SBLC cover ${req.tradeFinance.reference ?? req.idempotencyKey}`, + }); + await (0, swift_1.forwardSwiftToListener)(cover); + } + const advice = (0, swift_1.buildMt910Stub)({ + transactionRef: `${req.idempotencyKey}-ADV`, + relatedRef: req.idempotencyKey, + currency: req.currency, + amount: req.amount, + valueDate: req.valueDate, + accountWithInstitution: req.creditorBic, + remittance: `Credit confirmation ${req.beneficiaryName}`, + }); + await (0, swift_1.forwardSwiftToListener)(advice); } const iso = await (0, omnl_1.archiveIso20022)({ messageType: swiftKind === 'MT102' ? 'pacs.008' : 'pacs.008', diff --git a/services/settlement-middleware/dist/workflows/fiat-lp-to-liquidity.js b/services/settlement-middleware/dist/workflows/fiat-lp-to-liquidity.js new file mode 100644 index 0000000..8c17c9b --- /dev/null +++ b/services/settlement-middleware/dist/workflows/fiat-lp-to-liquidity.js @@ -0,0 +1,96 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.processFiatLpToLiquidity = processFiatLpToLiquidity; +const uuid_1 = require("uuid"); +const config_1 = require("../config"); +const omnl_1 = require("../adapters/omnl"); +const settlement_store_1 = require("../store/settlement-store"); +async function processFiatLpToLiquidity(input) { + const cfg = (0, config_1.loadConfig)(); + const officeId = (0, config_1.settlementOfficeId)(input.officeId); + const req = { ...input, officeId }; + const holder = req.holderAddress.trim(); + const recipient = (req.recipientAddress ?? holder).trim(); + 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: '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, patch = {}) => { + record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch }; + settlement_store_1.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 (0, omnl_1.requestFiatLpToLiquidity)({ + holderAddress: holder, + recipientAddress: recipient, + symbol: req.symbol, + amount: req.amount, + convertAll: req.convertAll, + maxSlippageBps: req.maxSlippageBps, + settlementRef: record.settlementId, + dryRun, + }); + const liquiditySwaps = 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, + 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; + } +} diff --git a/services/settlement-middleware/dist/workflows/z-bank-m0-m4.js b/services/settlement-middleware/dist/workflows/z-bank-m0-m4.js new file mode 100644 index 0000000..37a66f8 --- /dev/null +++ b/services/settlement-middleware/dist/workflows/z-bank-m0-m4.js @@ -0,0 +1,203 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.processZBankM0ToM4 = processZBankM0ToM4; +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"); +function toExternalRequest(input, officeId) { + return { + idempotencyKey: input.idempotencyKey, + officeId, + valueDate: input.valueDate ?? new Date().toISOString().slice(0, 10), + currency: input.currency ?? 'USD', + amount: input.amount, + creditorIban: input.creditorIban, + creditorBic: input.creditorBic, + beneficiaryName: input.beneficiaryName ?? 'Z Online Bank', + orderingName: 'Z Online Bank — OMNL Office 24', + remittanceInfo: input.remittanceInfo ?? 'Z Online Bank full settlement M0→M4', + moneyLayers: settlement_core_1.ALL_MONEY_LAYERS, + rail: 'SWIFT', + swiftMessageKind: 'MT103', + convertToCrypto: input.recipientAddress + ? { + enabled: true, + targetChainId: 138, + tokenLineId: input.tokenLineId ?? 'USD-M2', + recipientAddress: input.recipientAddress, + } + : undefined, + }; +} +async function processZBankM0ToM4(input) { + const cfg = (0, config_1.loadConfig)(); + const profile = (0, config_1.loadOfficeProfile)(); + const officeId = (0, config_1.settlementOfficeId)(input.officeId); + const includeM3 = Boolean(input.recipientAddress?.trim()); + const req = toExternalRequest(input, officeId); + 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: req, + layerSteps: [], + errors: [], + createdAt: now, + updatedAt: now, + }; + const advance = (phase, patch = {}) => { + record = { ...record, phase, updatedAt: new Date().toISOString(), ...patch }; + settlement_store_1.settlementStore.save(record); + }; + const glProfile = { + glM0: profile.settlement.glM0 ?? cfg.moneySupply.glM0 ?? '1050', + glM1: profile.settlement.glM1 ?? cfg.moneySupply.glM1 ?? '2100', + glM2: profile.settlement.glM2 ?? cfg.moneySupply.glM2 ?? '2200', + glM3: profile.settlement.glM3 ?? cfg.moneySupply.glM3 ?? '2300', + glM4NearMoney: profile.settlement.glM4NearMoney ?? cfg.moneySupply.glM4NearMoney ?? cfg.moneySupply.glSettlement ?? '1000', + }; + try { + if (!(0, settlement_core_1.isIbanValid)(req.creditorIban)) { + throw new Error(`Invalid creditor IBAN: ${req.creditorIban}`); + } + advance('VALIDATED'); + const verbiage = (0, settlement_core_1.rollExternalTransferVerbiage)(req); + advance('VERBIAGE_ROLLED', { verbiageDocument: verbiage }); + const amount = parseFloat(req.amount) || 0; + const hops = (0, settlement_core_1.zBankM0ToM4Hops)(includeM3); + const steps = []; + let lastJournalRef; + if (amount > 0) { + const balances = await (0, fineract_1.fetchGlBalances)(officeId); + const snap = (0, settlement_core_1.buildMoneySupplySnapshot)(officeId, balances); + for (const hop of hops) { + const pair = (0, settlement_core_1.resolveZBankLayerJournal)(hop.from, hop.to, glProfile); + if (!pair) { + steps.push({ + from: hop.from, + to: hop.to, + debitGl: '', + creditGl: '', + status: 'skipped', + }); + continue; + } + if (hop.to === 'M3') { + const { fullyBacked } = (0, settlement_core_1.m2FiatToTokenLoadAmount)(req.amount, snap.M2.broadMoney); + const m3Check = (0, settlement_core_1.m3TokenLoadAmount)(req.amount, snap.M2.broadMoney, snap.M3.tokenLiabilities); + if (!fullyBacked || !m3Check.fullyBacked) { + steps.push({ + from: hop.from, + to: hop.to, + debitGl: pair.debitGl, + creditGl: pair.creditGl, + status: 'failed', + }); + throw new Error(`M2/M3 backing insufficient (${m3Check.loadAmount}/${req.amount} M3 headroom)`); + } + } + const [debitGlId, creditGlId] = await Promise.all([ + (0, fineract_1.resolveGlAccountId)(pair.debitGl), + (0, fineract_1.resolveGlAccountId)(pair.creditGl), + ]); + const ref = `${req.idempotencyKey}-${hop.from}${hop.to}`; + const je = await (0, fineract_1.postJournalEntry)({ + officeId, + transactionDate: req.valueDate, + referenceNumber: ref, + comments: `${pair.narrative} · ${verbiage.slice(0, 200)}`, + debits: [{ glAccountId: debitGlId, amount }], + credits: [{ glAccountId: creditGlId, amount }], + }); + lastJournalRef = String(je.resourceId); + steps.push({ + from: hop.from, + to: hop.to, + debitGl: pair.debitGl, + creditGl: pair.creditGl, + journalRef: lastJournalRef, + status: 'posted', + }); + } + } + advance('FINERACT_POSTED', { fineractJournalRef: lastJournalRef, layerSteps: steps }); + if (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, + creditorIban: req.creditorIban, + creditorBic: req.creditorBic, + beneficiaryName: req.beneficiaryName, + remittanceInfo: req.remittanceInfo, + rail: req.rail, + }); + advance('HYBX_RAIL_DISPATCHED', { hybxPaymentId: pay.paymentId }); + } + else { + advance('HYBX_RAIL_DISPATCHED'); + } + const swiftRaw = (0, swift_1.buildMt103Stub)({ + senderRef: req.idempotencyKey, + currency: req.currency, + amount: req.amount, + valueDate: req.valueDate, + orderingCustomer: req.orderingName ?? cfg.omnlLegalName, + beneficiary: req.beneficiaryName, + iban: req.creditorIban, + bic: req.creditorBic, + remittance: req.remittanceInfo, + }); + if (cfg.rails.swift.enabled) { + 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 }); + if (req.convertToCrypto?.enabled && req.convertToCrypto.recipientAddress) { + const balances = await (0, fineract_1.fetchGlBalances)(officeId); + const snap = (0, settlement_core_1.buildMoneySupplySnapshot)(officeId, balances); + const { loadAmount, fullyBacked } = (0, settlement_core_1.m2FiatToTokenLoadAmount)(req.amount, snap.M2.broadMoney); + const m3Check = (0, settlement_core_1.m3TokenLoadAmount)(loadAmount, snap.M2.broadMoney, snap.M3.tokenLiabilities); + if (fullyBacked && m3Check.fullyBacked) { + const mint = await (0, omnl_1.requestTokenMint)({ + lineId: req.convertToCrypto.tokenLineId, + amount: m3Check.loadAmount, + recipient: req.convertToCrypto.recipientAddress, + settlementRef: record.settlementId, + dryRun: !cfg.production.allowChainMintExecute, + }); + advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash }); + } + else { + record.errors.push('M3 mint skipped — insufficient M2/M3 backing'); + advance('CHAIN_MINT_REQUESTED'); + } + } + else { + advance('CHAIN_MINT_REQUESTED'); + } + advance('SETTLED'); + return record; + } + catch (err) { + const msg = err instanceof Error ? err.message : String(err); + record.errors.push(msg); + advance('FAILED'); + return record; + } +} diff --git a/services/settlement-middleware/src/adapters/fineract.ts b/services/settlement-middleware/src/adapters/fineract.ts index fb3f2c2..63cf9c4 100644 --- a/services/settlement-middleware/src/adapters/fineract.ts +++ b/services/settlement-middleware/src/adapters/fineract.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { settlementStore } from '../store/settlement-store'; -const GL_HINTS = ['1000', '1050', '1410', '2000', '2100', '2200', '2300']; +const GL_HINTS = ['1000', '1050', '12015', '12424', '1410', '2000', '2100', '2200', '2300']; function fineractBase(): string { return (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, ''); @@ -148,7 +148,13 @@ export async function postJournalEntry(entry: { debits: { glAccountId: number; amount: number }[]; credits: { glAccountId: number; amount: number }[]; }): Promise<{ resourceId: number }> { - const { data } = await axios.post(`${fineractBase()}/journalentries`, entry, { + const payload = { + ...entry, + currencyCode: 'USD', + dateFormat: 'yyyy-MM-dd', + locale: 'en', + }; + const { data } = await axios.post(`${fineractBase()}/journalentries`, payload, { headers: authHeaders(), timeout: 60000, }); @@ -177,6 +183,22 @@ export async function resolveGlAccountId(glCode: string): Promise { return match.id; } +/** Resolve Fineract GL account ids for L1 BTC custody legs (HO 12015 / Office 24 12424). */ +export async function resolveBtcGlAccountIds(_officeId?: number): Promise<{ + custodyHoGlId: number; + interofficeBtcGlId: number; + custodyHoGlCode: string; + interofficeBtcGlCode: string; +}> { + const custodyHoGlCode = '12015'; + const interofficeBtcGlCode = '12424'; + const [custodyHoGlId, interofficeBtcGlId] = await Promise.all([ + resolveGlAccountId(custodyHoGlCode), + resolveGlAccountId(interofficeBtcGlCode), + ]); + return { custodyHoGlId, interofficeBtcGlId, custodyHoGlCode, interofficeBtcGlCode }; +} + export async function fetchGlBalances(officeId: number): Promise> { const base = fineractBase(); if (!base || !process.env.OMNL_FINERACT_PASSWORD) return {}; diff --git a/services/settlement-middleware/src/adapters/omnl.ts b/services/settlement-middleware/src/adapters/omnl.ts index 8020be8..4eb4116 100644 --- a/services/settlement-middleware/src/adapters/omnl.ts +++ b/services/settlement-middleware/src/adapters/omnl.ts @@ -90,3 +90,47 @@ export async function requestTokenTransfer(payload: { throw err; } } + +export type FiatLpLiquidityAdapterResult = { + swaps: Array<{ + lpSymbol: string; + tokenIn: string; + tokenOut: string; + realSymbol: string; + amountIn: string; + amountOutQuoted?: string; + txHash?: string; + status: string; + error?: string; + }>; + positions?: unknown[]; + dryRun: boolean; +}; + +export async function requestFiatLpToLiquidity(payload: { + holderAddress: string; + recipientAddress?: string; + symbol?: string; + amount?: string; + convertAll?: boolean; + maxSlippageBps?: number; + settlementRef: string; + dryRun: boolean; +}): Promise { + const base = (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, ''); + const key = process.env.OMNL_API_KEY || ''; + const headers: Record = { 'Content-Type': 'application/json' }; + if (key) headers.Authorization = `Bearer ${key}`; + + const { data } = await axios.post( + `${base}/api/v1/omnl/settlement/fiat-lp-to-liquidity`, + { ...payload, dryRun: payload.dryRun }, + { headers, timeout: 300000 }, + ); + return { + swaps: data.swaps ?? [], + positions: data.positions, + dryRun: Boolean(data.dryRun ?? payload.dryRun), + }; +} + diff --git a/services/settlement-middleware/src/adapters/pricing.ts b/services/settlement-middleware/src/adapters/pricing.ts new file mode 100644 index 0000000..a144b8f --- /dev/null +++ b/services/settlement-middleware/src/adapters/pricing.ts @@ -0,0 +1,86 @@ +import axios from 'axios'; + +const CBTC_ADDRESS = '0xe94260c555ac1d9d3cc9e1632883452ebdf0082e'; +const CHAIN_138 = 138; + +export type LiveBtcUsdQuote = { + btcUsdRate: number; + source: string; + asOf: string; + tokenAddress: string; +}; + +function tokenAggregationBase(): string { + return (process.env.TOKEN_AGGREGATION_URL || 'http://localhost:3000').replace(/\/$/, ''); +} + +/** + * Resolve live BTC/USD for settlement. Rejects unresolved/stale prices — + * production must not fall back to the $90k repo snapshot. + */ +export async function resolveLiveBtcUsd(): Promise { + const base = tokenAggregationBase(); + const key = process.env.OMNL_API_KEY || ''; + const headers: Record = { 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`; + + let data: unknown; + try { + const res = await axios.get(url, { headers, timeout: 30000 }); + data = res.data; + } catch (err) { + if (axios.isAxiosError(err)) { + const status = err.response?.status; + const body = err.response?.data; + const detail = + typeof body === 'string' + ? body.slice(0, 400) + : body != null + ? JSON.stringify(body).slice(0, 400) + : err.message; + throw new Error( + `Live BTC/USD fetch failed${status != null ? ` HTTP ${status}` : ''}: ${detail}`, + ); + } + throw err; + } + const map = (data ?? {}) as Record>; + const row = map[CBTC_ADDRESS.toLowerCase()] ?? map[CBTC_ADDRESS]; + 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.', + ); + } + + // Guard against known static repo fallback used in non-production + const requireLive = + process.env.OMNL_REQUIRE_LIVE_BTC_PRICE === '1' || process.env.NODE_ENV === 'production'; + if (requireLive && Math.abs(rate - 90000) < 0.01) { + // Allow only if explicitly overridden via env canonical + const envOverride = Number( + process.env.CHAIN138_CANONICAL_PRICE_USD_BTC || + process.env.CANONICAL_PRICE_USD_BTC || + process.env.BTC_PRICE_USD || + '', + ); + if (!(envOverride > 0 && Math.abs(envOverride - 90000) < 0.01)) { + throw new Error( + 'BTC/USD appears to be repo-fallback ($90000) — live oracle required for production settlement', + ); + } + } + + return { + btcUsdRate: rate, + source: 'token-aggregation/metamask-spot-prices', + asOf: new Date().toISOString(), + tokenAddress: CBTC_ADDRESS, + }; +} diff --git a/services/settlement-middleware/src/api/routes/settlement.ts b/services/settlement-middleware/src/api/routes/settlement.ts index 88659de..b2e933f 100644 --- a/services/settlement-middleware/src/api/routes/settlement.ts +++ b/services/settlement-middleware/src/api/routes/settlement.ts @@ -1,5 +1,14 @@ import { Router, type Request, type Response } from 'express'; -import type { ExternalTransferRequest, TradeFinanceInstrument, TokenLoadRequest, TransferRequest, ZBankM0M4Request } from '@dbis/settlement-core'; +import type { + BtcL1ReconcileRequest, + BtcL1SettlementRequest, + ExternalTransferRequest, + FiatLpToLiquidityRequest, + TradeFinanceInstrument, + TokenLoadRequest, + TransferRequest, + ZBankM0M4Request, +} from '@dbis/settlement-core'; import { buildMoneySupplySnapshot, loadOmnlChainRegistry, getActiveChains } from '@dbis/settlement-core'; import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../../config'; import { @@ -13,7 +22,13 @@ import { bindOffice24 } from '../../workflows/office-scope'; import { settlementStore } from '../../store/settlement-store'; import { processSwiftInbound } from '../../workflows/swift-inbound'; import { processZBankM0ToM4 } from '../../workflows/z-bank-m0-m4'; +import { processFiatLpToLiquidity } from '../../workflows/fiat-lp-to-liquidity'; +import { + processBtcL1Settlement, + reconcileBtcL1Settlement, +} from '../../workflows/btc-l1-settlement'; import { fetchGlBalances, fineractOfficeReachable } from '../../adapters/fineract'; +import { resolveLiveBtcUsd } from '../../adapters/pricing'; import { requireApiKey } from '../../middleware/auth'; async function respondPublicMoneySupply(res: Response, officeId: number): Promise { @@ -275,6 +290,28 @@ export function createSettlementRouter(): Router { } }); + router.post('/convert/fiat-lp-to-liquidity', async (req, res) => { + if (!requireApiKey(req, res)) return; + try { + const body = req.body as FiatLpToLiquidityRequest; + if (!body.idempotencyKey || !body.holderAddress?.startsWith('0x')) { + res.status(400).json({ error: 'idempotencyKey and holderAddress (0x) required' }); + return; + } + if (!body.convertAll && !body.symbol) { + res.status(400).json({ error: 'symbol required unless convertAll is true' }); + return; + } + const record = await processFiatLpToLiquidity({ + ...body, + officeId: settlementOfficeId(body.officeId), + }); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + router.post('/transfer/internal', async (req, res) => { if (!requireApiKey(req, res)) return; try { @@ -354,5 +391,123 @@ export function createSettlementRouter(): Router { } }); + router.post('/btc/l1-settle', async (req, res) => { + if (!requireApiKey(req, res)) return; + try { + const body = req.body as BtcL1SettlementRequest; + if ( + !body.idempotencyKey || + !body.instructionId || + !body.txId || + !body.depositAddress || + body.amountSats == null || + !body.recipientAddress + ) { + res.status(400).json({ + error: + 'idempotencyKey, instructionId, txId, depositAddress, amountSats, recipientAddress required', + }); + return; + } + const record = await processBtcL1Settlement({ + ...body, + officeId: settlementOfficeId(body.officeId), + confirmations: Number(body.confirmations ?? 0), + amountSats: Number(body.amountSats), + }); + res.status(record.phase === 'FAILED' ? 422 : 200).json(record); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + /** + * Mark FAILED/partial BTC L1 settle as SETTLED when books + mint already done. + * Does not re-journal or remint. + */ + router.post('/btc/l1-reconcile', (req, res) => { + if (!requireApiKey(req, res)) return; + try { + const body = req.body as BtcL1ReconcileRequest; + if ( + (!body.settlementId && !body.idempotencyKey) || + body.amountSats == null || + !body.txId || + !body.depositAddress || + !body.instructionId || + !body.recipientAddress || + !body.chainTxHash || + body.btcUsdRate == null + ) { + res.status(400).json({ + error: + 'settlementId|idempotencyKey, amountSats, txId, depositAddress, instructionId, recipientAddress, chainTxHash, btcUsdRate required', + }); + return; + } + const record = reconcileBtcL1Settlement({ + ...body, + amountSats: Number(body.amountSats), + btcUsdRate: Number(body.btcUsdRate), + }); + res.status(200).json(record); + } catch (e) { + res.status(400).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + + router.get('/btc/settlements', (req, res) => { + if (!requireApiKey(req, res)) return; + const limit = Math.min(Number(req.query.limit) || 50, 500); + const rows = settlementStore + .list(limit * 4) + .filter((r) => r.btcSettlement != null) + .slice(0, limit); + res.json({ count: rows.length, settlements: rows }); + }); + + router.get('/btc/ledger', async (_req, res) => { + if (!requireApiKey(_req, res)) return; + try { + const [balancesHo, balancesO24, quote] = await Promise.all([ + fetchGlBalances(1), + fetchGlBalances(officeId), + resolveLiveBtcUsd().catch((e) => ({ + error: e instanceof Error ? e.message : String(e), + })), + ]); + const btcSettlements = settlementStore + .list(500) + .filter((r) => r.btcSettlement != null); + const outstandingSats = btcSettlements + .filter((r) => r.phase === 'CHAIN_MINT_REQUESTED' || r.phase === 'FINERACT_POSTED') + .reduce((sum, r) => sum + (r.btcSettlement?.amountSats ?? 0), 0); + const settledSats = btcSettlements + .filter((r) => r.phase === 'SETTLED') + .reduce((sum, r) => sum + (r.btcSettlement?.amountSats ?? 0), 0); + + res.json({ + asOf: new Date().toISOString(), + gl: { + custodyHo12015: balancesHo['12015'] ?? '0', + interofficeBtc12424: balancesHo['12424'] ?? balancesO24['12424'] ?? '0', + m2Broad2200: balancesO24['2200'] ?? '0', + m3Token2300: balancesO24['2300'] ?? '0', + }, + btcUsd: + 'btcUsdRate' in quote + ? { rate: quote.btcUsdRate, source: quote.source, asOf: quote.asOf } + : { error: (quote as { error: string }).error }, + mintQueue: { + outstandingSats, + settledSats, + settlementCount: btcSettlements.length, + }, + }); + } catch (e) { + res.status(500).json({ error: e instanceof Error ? e.message : String(e) }); + } + }); + return router; } diff --git a/services/settlement-middleware/src/store/settlement-store.ts b/services/settlement-middleware/src/store/settlement-store.ts index 7adcd22..b8fac18 100644 --- a/services/settlement-middleware/src/store/settlement-store.ts +++ b/services/settlement-middleware/src/store/settlement-store.ts @@ -38,8 +38,9 @@ export class SettlementStore { return fs .readdirSync(dir) .filter((f) => f.endsWith('.json') && !f.startsWith('key-')) - .slice(0, limit) - .map((f) => JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')) as SettlementRecord); + .map((f) => JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')) as SettlementRecord) + .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt))) + .slice(0, limit); } } diff --git a/services/settlement-middleware/src/workflows/btc-l1-settlement.test.ts b/services/settlement-middleware/src/workflows/btc-l1-settlement.test.ts new file mode 100644 index 0000000..7c94e17 --- /dev/null +++ b/services/settlement-middleware/src/workflows/btc-l1-settlement.test.ts @@ -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; diff --git a/services/settlement-middleware/src/workflows/btc-l1-settlement.ts b/services/settlement-middleware/src/workflows/btc-l1-settlement.ts new file mode 100644 index 0000000..4470727 --- /dev/null +++ b/services/settlement-middleware/src/workflows/btc-l1-settlement.ts @@ -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 { + 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 = {}) => { + 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; +} diff --git a/services/settlement-middleware/src/workflows/fiat-lp-to-liquidity.ts b/services/settlement-middleware/src/workflows/fiat-lp-to-liquidity.ts new file mode 100644 index 0000000..7c46a61 --- /dev/null +++ b/services/settlement-middleware/src/workflows/fiat-lp-to-liquidity.ts @@ -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 { + 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 = {}) => { + 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; + } +} diff --git a/services/token-aggregation/public/cbtc-landing.html b/services/token-aggregation/public/cbtc-landing.html new file mode 100644 index 0000000..03e3046 --- /dev/null +++ b/services/token-aggregation/public/cbtc-landing.html @@ -0,0 +1,43 @@ + +1000 cBTC — hot production rails + +
+

1000 cBTChot · settled · pool-deep

+

Production rails status

+

Ledger reconciled SETTLED · cUSDT PMM ≥1000 cBTC · classic DODO trade verified.

+ + + + + + + + + +
LocationcBTCRole
Holder 0xa55A…4882~1000Primary mint destination
cUSDT PMM 0x481C…3a8d1000.0HOTTradable depth + 105M cUSDT
cXAUC PMM~68Secondary
cUSDC PMM~8Thin (no cUSDC mint)
Fineract GL 12015~$64.82M≈1000 BTC custody
+

+ Settlement: POST /btc/l1-reconcile → record 60ec0214… SETTLED (1000 BTC, mint 0x594c13…).
+ Trade: transfer cBTC to pool then sellBase(trader) (DODO DVM classic).
+ Caps: swappable / convertible / transferableInternal / transferableExternal.
+ Smoke: node scripts/deployment/smoke-btc-settlement.mjs · LP: HOT_CBTC_EXECUTE=1 node scripts/deployment/hot-cbtc-liquidity-1000.mjs
+ Dashboards: /cbtc · /reserve · /omnl/dashboard +

+
diff --git a/services/token-aggregation/src/api/routes/omnl.ts b/services/token-aggregation/src/api/routes/omnl.ts index 5358767..1d64bc9 100644 --- a/services/token-aggregation/src/api/routes/omnl.ts +++ b/services/token-aggregation/src/api/routes/omnl.ts @@ -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; diff --git a/services/token-aggregation/src/api/server.ts b/services/token-aggregation/src/api/server.ts index 92ceb2a..41b6b20 100644 --- a/services/token-aggregation/src/api/server.ts +++ b/services/token-aggregation/src/api/server.ts @@ -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({ diff --git a/services/token-aggregation/src/config/fiat-lp-liquidity.ts b/services/token-aggregation/src/config/fiat-lp-liquidity.ts new file mode 100644 index 0000000..55cd6c0 --- /dev/null +++ b/services/token-aggregation/src/config/fiat-lp-liquidity.ts @@ -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; + 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'; +} diff --git a/services/token-aggregation/src/services/canonical-price-oracle.ts b/services/token-aggregation/src/services/canonical-price-oracle.ts index b0e1bda..e48c046 100644 --- a/services/token-aggregation/src/services/canonical-price-oracle.ts +++ b/services/token-aggregation/src/services/canonical-price-oracle.ts @@ -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 { diff --git a/services/token-aggregation/src/services/omnl-fiat-lp-liquidity.ts b/services/token-aggregation/src/services/omnl-fiat-lp-liquidity.ts new file mode 100644 index 0000000..d5b45f7 --- /dev/null +++ b/services/token-aggregation/src/services/omnl-fiat-lp-liquidity.ts @@ -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 { + 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 { + 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 { + 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 }; +} diff --git a/services/token-aggregation/src/services/omnl-ho-liquidity-enrichment.ts b/services/token-aggregation/src/services/omnl-ho-liquidity-enrichment.ts index 1177ba4..d7a4789 100644 --- a/services/token-aggregation/src/services/omnl-ho-liquidity-enrichment.ts +++ b/services/token-aggregation/src/services/omnl-ho-liquidity-enrichment.ts @@ -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 { + 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, + }; +} diff --git a/services/token-aggregation/src/services/omnl-ho-liquidity-snapshot.ts b/services/token-aggregation/src/services/omnl-ho-liquidity-snapshot.ts index 02a1a9f..5f99f2f 100644 --- a/services/token-aggregation/src/services/omnl-ho-liquidity-snapshot.ts +++ b/services/token-aggregation/src/services/omnl-ho-liquidity-snapshot.ts @@ -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(), diff --git a/services/token-aggregation/src/services/omnl-settlement-chain.ts b/services/token-aggregation/src/services/omnl-settlement-chain.ts index 5eed720..3b4499d 100644 --- a/services/token-aggregation/src/services/omnl-settlement-chain.ts +++ b/services/token-aggregation/src/services/omnl-settlement-chain.ts @@ -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 { + 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; +} + +/** 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 }; +}