feat(settlement): production wallet L1 BTC to cBTC (not institutional ledger)
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 51s
CI/CD Pipeline / Security Scanning (push) Has started running
CI/CD Pipeline / Lint and Format (push) Has been cancelled
CI/CD Pipeline / Terraform Validation (push) Has been cancelled
CI/CD Pipeline / Kubernetes Validation (push) Has been cancelled
Deploy ChainID 138 / Deploy ChainID 138 (push) Has been cancelled
Validation / validate-genesis (push) Has been cancelled
Validation / validate-terraform (push) Has been cancelled
Validation / validate-kubernetes (push) Has been cancelled
Validation / validate-smart-contracts (push) Has been cancelled
Validation / validate-security (push) Has been cancelled
Validation / validate-documentation (push) Has been cancelled
Verify Deployment / Verify Deployment (push) Has been cancelled
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 51s
CI/CD Pipeline / Security Scanning (push) Has started running
CI/CD Pipeline / Lint and Format (push) Has been cancelled
CI/CD Pipeline / Terraform Validation (push) Has been cancelled
CI/CD Pipeline / Kubernetes Validation (push) Has been cancelled
Deploy ChainID 138 / Deploy ChainID 138 (push) Has been cancelled
Validation / validate-genesis (push) Has been cancelled
Validation / validate-terraform (push) Has been cancelled
Validation / validate-kubernetes (push) Has been cancelled
Validation / validate-smart-contracts (push) Has been cancelled
Validation / validate-security (push) Has been cancelled
Validation / validate-documentation (push) Has been cancelled
Verify Deployment / Verify Deployment (push) Has been cancelled
Add wallet settlement source with custody-backed mint path, btc-intake persistence, /btc/wallet/l1-settle and /btc/wallet/status APIs, and full production deploy script. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
71
config/btc-wallet-l1.chain138.v1.json
Normal file
71
config/btc-wallet-l1.chain138.v1.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"$schema": "OMNL wallet L1 BTC → cBTC (normal bitcoind deposits, not institutional ledger program)",
|
||||
"version": "1.0.0",
|
||||
"updated": "2026-07-21",
|
||||
"chainId": 138,
|
||||
"officeId": 24,
|
||||
"settlementSource": "wallet",
|
||||
"confirmationsRequired": 6,
|
||||
"maxDepositSats": 100000000000,
|
||||
"backingMode": "custody_fair_value",
|
||||
"token": {
|
||||
"symbol": "cBTC",
|
||||
"address": "0xe94260c555aC1d9D3CC9E1632883452ebDf0082E",
|
||||
"decimals": 8,
|
||||
"omnlLine": "BTC-M2",
|
||||
"lineId": "BTC-M2"
|
||||
},
|
||||
"recipients": {
|
||||
"defaultVault": "0xeb389B0FeB3186e88002e51b404685081C8dB8Ff",
|
||||
"notes": "Per-deposit recipient comes from btc-intake basketMandate.chain138VaultAddress"
|
||||
},
|
||||
"custody": {
|
||||
"debitGl": "12015",
|
||||
"creditGl": "12424",
|
||||
"memo": "T-BTC-WALLET-L1"
|
||||
},
|
||||
"fineractGl": {
|
||||
"custodyHo12015": { "glCode": "12015", "officeId": 1, "memo": "T-BTC-WALLET-L1" },
|
||||
"interofficeBtc12424": { "glCode": "12424", "officeId": 1, "memo": "T-BTC-WALLET-L1" },
|
||||
"m2Broad2200": { "glCode": "2200", "officeId": 24, "memo": "T-BTC-WALLET-M2M3" },
|
||||
"m3Token2300": { "glCode": "2300", "officeId": 24, "memo": "T-BTC-WALLET-M2M3" }
|
||||
},
|
||||
"oracle": {
|
||||
"aggregator": "0x99b3511a2d315a497c8112c1fdd8d508d4b1e506",
|
||||
"proxy": "0x3304b747E565a97ec8AC220b0B6A1f6ffDB837e6",
|
||||
"reserveSystem": "0x607e97cD626f209facfE48c1464815DDE15B5093",
|
||||
"priceFeedKeeper": "0xD3AD6831aacB5386B8A25BB8D8176a6C8a026f04"
|
||||
},
|
||||
"dodoEvm": {
|
||||
"pmmIntegration": "0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895"
|
||||
},
|
||||
"pmmPools": {
|
||||
"cBTC_cUSDT": "0x481CcE1046e1F1ab13BC37f0f374bb5ff75F3a8d",
|
||||
"cBTC_cUSDC": "0xD8e6131Fe469F0450b19ef71c9878bf9CFd4a5BD",
|
||||
"cBTC_cXAUC": "0xBb7653746EC39809C8DE27C52fD858d7E32e6A77"
|
||||
},
|
||||
"quoteTokens": {
|
||||
"cUSDT": "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22",
|
||||
"cUSDC": "0xf22258f57794CC8E06237084b353Ab30fFfa640b",
|
||||
"cXAUC": "0x290E52a8819A4fbD0714E517225429aA2B70EC6b"
|
||||
},
|
||||
"rpc": {
|
||||
"envVar": "RPC_URL_138",
|
||||
"public": "https://rpc-http-pub.d-bis.org",
|
||||
"explorer": "https://explorer.d-bis.org"
|
||||
},
|
||||
"settlementApi": {
|
||||
"walletL1Settle": "/btc/wallet/l1-settle",
|
||||
"walletStatus": "/btc/wallet/status",
|
||||
"depositInstructions": "http://127.0.0.1:3013/deposit-instructions",
|
||||
"productionBase": "https://secure.omdnl.org/settlement"
|
||||
},
|
||||
"envKeys": {
|
||||
"config": "OMNL_BTC_WALLET_CONFIG",
|
||||
"cbtcAddress": "CBTC_ADDRESS_138",
|
||||
"mintOperator": "OMNL_MINT_OPERATOR_PRIVATE_KEY",
|
||||
"rpc138": "RPC_URL_138",
|
||||
"allowMint": "SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE",
|
||||
"bitcoindUrl": "BITCOIND_RPC_URL"
|
||||
}
|
||||
}
|
||||
@@ -129,8 +129,9 @@ ORACLE_PROXY_ADDRESS=0x3304b747E565a97ec8AC220b0B6A1f6ffDB837e6
|
||||
CHAIN138_POOL_CBTC_CUSDT=0x481CcE1046e1F1ab13BC37f0f374bb5ff75F3a8d
|
||||
CHAIN138_POOL_CBTC_CUSDC=0xD8e6131Fe469F0450b19ef71c9878bf9CFd4a5BD
|
||||
CHAIN138_POOL_CBTC_CXAUC=0xBb7653746EC39809C8DE27C52fD858d7E32e6A77
|
||||
OMNL_BTC_WALLET_CONFIG=config/btc-wallet-l1.chain138.v1.json
|
||||
OMNL_MINT_OPERATOR_PRIVATE_KEY=
|
||||
SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=0
|
||||
SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=1
|
||||
|
||||
# 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
|
||||
|
||||
@@ -67,6 +67,16 @@
|
||||
"ledgerEndpoint": "/btc/ledger",
|
||||
"l1SettleEndpoint": "/btc/l1-settle"
|
||||
},
|
||||
"btcWallet": {
|
||||
"configPath": "config/btc-wallet-l1.chain138.v1.json",
|
||||
"settlementSource": "wallet",
|
||||
"backingMode": "custody_fair_value",
|
||||
"confirmationsRequired": 6,
|
||||
"walletSettleEndpoint": "/btc/wallet/l1-settle",
|
||||
"walletStatusEndpoint": "/btc/wallet/status",
|
||||
"btcIntakePort": 3013,
|
||||
"notes": "Normal bitcoind wallet deposits → custody journal + cBTC mint. Not subject to institutional 1,000 BTC headroom gate."
|
||||
},
|
||||
"guosmm": {
|
||||
"emcbCharter": "GUOSMM-B-2026-001",
|
||||
"registryPath": "config/guosmm-notices.v1.json",
|
||||
|
||||
@@ -56,6 +56,7 @@ services:
|
||||
- SETTLEMENT_MIDDLEWARE_PORT=3011
|
||||
- SETTLEMENT_MIDDLEWARE_CONFIG=/app/config/settlement-middleware.production.v1.json
|
||||
- OMNL_BTC_LEDGER_CONFIG=/app/config/btc-l1-ledger-1000.chain138.v1.json
|
||||
- OMNL_BTC_WALLET_CONFIG=/app/config/btc-wallet-l1.chain138.v1.json
|
||||
- OMNL_SUPPORTED_CHAINS_CONFIG=/app/config/omnl-supported-chains.v1.json
|
||||
- TOKEN_AGGREGATION_URL=http://token-aggregation:3000
|
||||
- CHAIN138_ORACLE_URL=http://chain138-oracle:3500
|
||||
|
||||
@@ -23,4 +23,14 @@ const money_supply_1 = require("./money-supply");
|
||||
strict_1.default.equal(j.creditGl, '12424');
|
||||
strict_1.default.equal(j.memo, 'T-BTC-L1-HO');
|
||||
});
|
||||
(0, node_test_1.it)('wallet custody-backed load accepts fair-value deposit within cap', () => {
|
||||
const ok = (0, money_supply_1.walletBtcCustodyBackedLoad)('65000.00', 10_000_000_000, 100_000_000);
|
||||
strict_1.default.equal(ok.fullyBacked, true);
|
||||
strict_1.default.equal(ok.loadAmount, '65000.00');
|
||||
});
|
||||
(0, node_test_1.it)('wallet custody-backed load rejects over max deposit sats', () => {
|
||||
const bad = (0, money_supply_1.walletBtcCustodyBackedLoad)('95000.00', 50_000_000, 100_000_000);
|
||||
strict_1.default.equal(bad.fullyBacked, false);
|
||||
strict_1.default.match(bad.reason ?? '', /exceeds wallet max/i);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,15 @@ export declare function m2FiatToTokenLoadAmount(fiatAmount: string, m2Available:
|
||||
loadAmount: string;
|
||||
fullyBacked: boolean;
|
||||
};
|
||||
/**
|
||||
* Wallet L1 BTC deposit: custody journal (12015/12424) backs cBTC mint at IFRS 13 fair value.
|
||||
* Skips institutional M2−M3 headroom gate — each confirmed on-chain deposit is self-backed.
|
||||
*/
|
||||
export declare function walletBtcCustodyBackedLoad(usdValue: string, maxDepositSats?: number, amountSats?: number): {
|
||||
loadAmount: string;
|
||||
fullyBacked: boolean;
|
||||
reason?: string;
|
||||
};
|
||||
/** M3 on-chain token mint must not exceed outstanding M2 broad money backing. */
|
||||
export declare function m3TokenLoadAmount(requested: string, m2Broad: string, m3Outstanding: string): {
|
||||
loadAmount: string;
|
||||
|
||||
19
packages/settlement-core/dist/money-supply.js
vendored
19
packages/settlement-core/dist/money-supply.js
vendored
@@ -4,6 +4,7 @@ exports.ALL_MONEY_LAYERS = exports.POLICY = exports.GL_CODES = void 0;
|
||||
exports.aggregateMetaFiatLayers = aggregateMetaFiatLayers;
|
||||
exports.buildMoneySupplySnapshot = buildMoneySupplySnapshot;
|
||||
exports.m2FiatToTokenLoadAmount = m2FiatToTokenLoadAmount;
|
||||
exports.walletBtcCustodyBackedLoad = walletBtcCustodyBackedLoad;
|
||||
exports.m3TokenLoadAmount = m3TokenLoadAmount;
|
||||
exports.resolveZBankLayerJournal = resolveZBankLayerJournal;
|
||||
exports.zBankM0ToM4Hops = zBankM0ToM4Hops;
|
||||
@@ -98,6 +99,24 @@ function m2FiatToTokenLoadAmount(fiatAmount, m2Available) {
|
||||
fullyBacked: load >= fiat && fiat > 0,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Wallet L1 BTC deposit: custody journal (12015/12424) backs cBTC mint at IFRS 13 fair value.
|
||||
* Skips institutional M2−M3 headroom gate — each confirmed on-chain deposit is self-backed.
|
||||
*/
|
||||
function walletBtcCustodyBackedLoad(usdValue, maxDepositSats, amountSats) {
|
||||
const fiat = parseFloat(usdValue) || 0;
|
||||
if (!(fiat > 0)) {
|
||||
return { loadAmount: '0.00', fullyBacked: false, reason: 'USD value must be positive' };
|
||||
}
|
||||
if (maxDepositSats != null && maxDepositSats > 0 && amountSats != null && amountSats > maxDepositSats) {
|
||||
return {
|
||||
loadAmount: '0.00',
|
||||
fullyBacked: false,
|
||||
reason: `Deposit ${amountSats} sats exceeds wallet max ${maxDepositSats} sats`,
|
||||
};
|
||||
}
|
||||
return { loadAmount: fiat.toFixed(2), fullyBacked: true };
|
||||
}
|
||||
/** M3 on-chain token mint must not exceed outstanding M2 broad money backing. */
|
||||
function m3TokenLoadAmount(requested, m2Broad, m3Outstanding) {
|
||||
const req = parseFloat(requested) || 0;
|
||||
|
||||
4
packages/settlement-core/dist/types.d.ts
vendored
4
packages/settlement-core/dist/types.d.ts
vendored
@@ -102,6 +102,8 @@ export type FiatLpToLiquidityRequest = {
|
||||
maxSlippageBps?: number;
|
||||
dryRun?: boolean;
|
||||
};
|
||||
/** Wallet = real L1 bitcoind deposit; ledger = institutional 1,000 BTC program. */
|
||||
export type BtcL1SettlementSource = 'wallet' | 'ledger';
|
||||
/** L1 Bitcoin deposit confirmed → Fineract custody journal + cBTC mint. */
|
||||
export type BtcL1SettlementRequest = {
|
||||
idempotencyKey: string;
|
||||
@@ -115,6 +117,8 @@ export type BtcL1SettlementRequest = {
|
||||
/** Chain 138 vault for cBTC mint. */
|
||||
recipientAddress: string;
|
||||
valueDate?: string;
|
||||
/** wallet = custody-backed mint (btc-intake); ledger = M2/M3 headroom program. */
|
||||
settlementSource?: BtcL1SettlementSource;
|
||||
};
|
||||
/**
|
||||
* Mark a FAILED/partial BTC L1 settle as SETTLED when books + mint already
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
btcUsdToJournalAmount,
|
||||
resolveBtcL1CustodyJournal,
|
||||
satsToBtcDecimal,
|
||||
walletBtcCustodyBackedLoad,
|
||||
} from './money-supply';
|
||||
|
||||
describe('BTC settlement helpers', () => {
|
||||
@@ -25,4 +26,16 @@ describe('BTC settlement helpers', () => {
|
||||
assert.equal(j.creditGl, '12424');
|
||||
assert.equal(j.memo, 'T-BTC-L1-HO');
|
||||
});
|
||||
|
||||
it('wallet custody-backed load accepts fair-value deposit within cap', () => {
|
||||
const ok = walletBtcCustodyBackedLoad('65000.00', 10_000_000_000, 100_000_000);
|
||||
assert.equal(ok.fullyBacked, true);
|
||||
assert.equal(ok.loadAmount, '65000.00');
|
||||
});
|
||||
|
||||
it('wallet custody-backed load rejects over max deposit sats', () => {
|
||||
const bad = walletBtcCustodyBackedLoad('95000.00', 50_000_000, 100_000_000);
|
||||
assert.equal(bad.fullyBacked, false);
|
||||
assert.match(bad.reason ?? '', /exceeds wallet max/i);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,6 +100,29 @@ export function m2FiatToTokenLoadAmount(fiatAmount: string, m2Available: string)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wallet L1 BTC deposit: custody journal (12015/12424) backs cBTC mint at IFRS 13 fair value.
|
||||
* Skips institutional M2−M3 headroom gate — each confirmed on-chain deposit is self-backed.
|
||||
*/
|
||||
export function walletBtcCustodyBackedLoad(usdValue: string, maxDepositSats?: number, amountSats?: number): {
|
||||
loadAmount: string;
|
||||
fullyBacked: boolean;
|
||||
reason?: string;
|
||||
} {
|
||||
const fiat = parseFloat(usdValue) || 0;
|
||||
if (!(fiat > 0)) {
|
||||
return { loadAmount: '0.00', fullyBacked: false, reason: 'USD value must be positive' };
|
||||
}
|
||||
if (maxDepositSats != null && maxDepositSats > 0 && amountSats != null && amountSats > maxDepositSats) {
|
||||
return {
|
||||
loadAmount: '0.00',
|
||||
fullyBacked: false,
|
||||
reason: `Deposit ${amountSats} sats exceeds wallet max ${maxDepositSats} sats`,
|
||||
};
|
||||
}
|
||||
return { loadAmount: fiat.toFixed(2), fullyBacked: true };
|
||||
}
|
||||
|
||||
/** M3 on-chain token mint must not exceed outstanding M2 broad money backing. */
|
||||
export function m3TokenLoadAmount(requested: string, m2Broad: string, m3Outstanding: string): {
|
||||
loadAmount: string;
|
||||
|
||||
@@ -134,6 +134,9 @@ export type FiatLpToLiquidityRequest = {
|
||||
dryRun?: boolean;
|
||||
};
|
||||
|
||||
/** Wallet = real L1 bitcoind deposit; ledger = institutional 1,000 BTC program. */
|
||||
export type BtcL1SettlementSource = 'wallet' | 'ledger';
|
||||
|
||||
/** L1 Bitcoin deposit confirmed → Fineract custody journal + cBTC mint. */
|
||||
export type BtcL1SettlementRequest = {
|
||||
idempotencyKey: string;
|
||||
@@ -147,6 +150,8 @@ export type BtcL1SettlementRequest = {
|
||||
/** Chain 138 vault for cBTC mint. */
|
||||
recipientAddress: string;
|
||||
valueDate?: string;
|
||||
/** wallet = custody-backed mint (btc-intake); ledger = M2/M3 headroom program. */
|
||||
settlementSource?: BtcL1SettlementSource;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
123
scripts/deployment/deploy-btc-wallet-production.sh
Normal file
123
scripts/deployment/deploy-btc-wallet-production.sh
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
# Full production deploy: wallet L1 BTC → cBTC (settlement-middleware + btc-intake).
|
||||
#
|
||||
# Run on dev-bis-ali / secure.omdnl.org backend:
|
||||
# bash scripts/deployment/deploy-btc-wallet-production.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="${OMNL_BANK_ROOT:-$HOME/smom-dbis-138}"
|
||||
ENV_FILE="${OMNL_BANK_ENV:-$REPO_DIR/.env}"
|
||||
LOG_DIR="${OMNL_BANK_LOG_DIR:-$HOME/omnl-bank/logs}"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $*"; }
|
||||
|
||||
cd "$REPO_DIR"
|
||||
|
||||
if [[ "${OMNL_SKIP_GIT_PULL:-0}" != "1" ]]; then
|
||||
log "Pull latest main..."
|
||||
git fetch origin && git checkout main && git pull --ff-only origin main
|
||||
fi
|
||||
|
||||
export SETTLEMENT_MIDDLEWARE_CONFIG="${SETTLEMENT_MIDDLEWARE_CONFIG:-$REPO_DIR/config/settlement-middleware.production.v1.json}"
|
||||
export OMNL_BTC_LEDGER_CONFIG="${OMNL_BTC_LEDGER_CONFIG:-$REPO_DIR/config/btc-l1-ledger-1000.chain138.v1.json}"
|
||||
export OMNL_BTC_WALLET_CONFIG="${OMNL_BTC_WALLET_CONFIG:-$REPO_DIR/config/btc-wallet-l1.chain138.v1.json}"
|
||||
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
set -a
|
||||
set +u
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set -u
|
||||
set +a
|
||||
fi
|
||||
|
||||
if [[ -f "$REPO_DIR/scripts/deployment/sync-btc-ledger-contract-env.mjs" ]]; then
|
||||
node "$REPO_DIR/scripts/deployment/sync-btc-ledger-contract-env.mjs" | while IFS= read -r line; do
|
||||
key="${line%%=*}"
|
||||
grep -q "^${key}=" "$ENV_FILE" 2>/dev/null || echo "$line" >>"$ENV_FILE"
|
||||
done
|
||||
set -a
|
||||
set +u
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set -u
|
||||
set +a
|
||||
fi
|
||||
|
||||
log "Building settlement-core, settlement-middleware, btc-intake..."
|
||||
for dir in packages/settlement-core services/settlement-middleware services/btc-intake; do
|
||||
cd "$REPO_DIR/$dir"
|
||||
NODE_ENV=development npm install --no-fund --no-audit
|
||||
npm run build
|
||||
done
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
start_settlement() {
|
||||
cd "$REPO_DIR/services/settlement-middleware"
|
||||
pkill -f "smom-dbis-138/services/settlement-middleware" 2>/dev/null || true
|
||||
fuser -k 3011/tcp 2>/dev/null || true
|
||||
sleep 1
|
||||
nohup env \
|
||||
SETTLEMENT_MIDDLEWARE_CONFIG="$SETTLEMENT_MIDDLEWARE_CONFIG" \
|
||||
OMNL_BTC_LEDGER_CONFIG="$OMNL_BTC_LEDGER_CONFIG" \
|
||||
OMNL_BTC_WALLET_CONFIG="$OMNL_BTC_WALLET_CONFIG" \
|
||||
OMNL_SUPPORTED_CHAINS_CONFIG="${OMNL_SUPPORTED_CHAINS_CONFIG:-$REPO_DIR/config/omnl-supported-chains.v1.json}" \
|
||||
OMNL_M2_TOKEN_REGISTRY="${OMNL_M2_TOKEN_REGISTRY:-$REPO_DIR/config/omnl-m2-token-registry.v1.json}" \
|
||||
OMNL_API_KEY="${OMNL_API_KEY:-${OMNL_SUPER_ADMIN_SECURE_KEY:-}}" \
|
||||
OMNL_FINERACT_BASE_URL="${OMNL_FINERACT_BASE_URL:-${FINERACT_BASE_URL:-}}" \
|
||||
OMNL_FINERACT_TENANT="${OMNL_FINERACT_TENANT:-${FINERACT_TENANT:-omnl}}" \
|
||||
OMNL_FINERACT_USER="${OMNL_FINERACT_USER:-}" \
|
||||
OMNL_FINERACT_USERNAME="${OMNL_FINERACT_USERNAME:-}" \
|
||||
OMNL_FINERACT_PASSWORD="${OMNL_FINERACT_PASSWORD:-}" \
|
||||
SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE="${SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE:-1}" \
|
||||
TOKEN_AGGREGATION_URL="${TOKEN_AGGREGATION_URL:-http://127.0.0.1:3000}" \
|
||||
CHAIN138_ORACLE_URL="${CHAIN138_ORACLE_URL:-http://127.0.0.1:3500}" \
|
||||
RPC_URL_138="${RPC_URL_138:-${CHAIN_138_RPC_URL:-}}" \
|
||||
OMNL_MINT_OPERATOR_PRIVATE_KEY="${OMNL_MINT_OPERATOR_PRIVATE_KEY:-}" \
|
||||
OMNL_REQUIRE_LIVE_BTC_PRICE="${OMNL_REQUIRE_LIVE_BTC_PRICE:-1}" \
|
||||
CBTC_ADDRESS_138="${CBTC_ADDRESS_138:-}" \
|
||||
BTC_L1_RECIPIENT_ADDRESS="${BTC_L1_RECIPIENT_ADDRESS:-}" \
|
||||
DODO_PMM_INTEGRATION="${DODO_PMM_INTEGRATION:-}" \
|
||||
npm run start >"$LOG_DIR/settlement-middleware.log" 2>&1 &
|
||||
echo $! >"$LOG_DIR/settlement-middleware.pid"
|
||||
}
|
||||
|
||||
start_btc_intake() {
|
||||
cd "$REPO_DIR/services/btc-intake"
|
||||
pkill -f "smom-dbis-138/services/btc-intake" 2>/dev/null || true
|
||||
fuser -k 3013/tcp 2>/dev/null || true
|
||||
sleep 1
|
||||
nohup env \
|
||||
BTC_INTAKE_PORT="${BTC_INTAKE_PORT:-3013}" \
|
||||
BTC_INTAKE_DATA_DIR="${BTC_INTAKE_DATA_DIR:-$REPO_DIR/data/btc-intake}" \
|
||||
SETTLEMENT_MIDDLEWARE_URL="${SETTLEMENT_MIDDLEWARE_URL:-http://127.0.0.1:3011}" \
|
||||
OMNL_API_KEY="${OMNL_API_KEY:-${OMNL_SUPER_ADMIN_SECURE_KEY:-}}" \
|
||||
BITCOIND_RPC_URL="${BITCOIND_RPC_URL:-}" \
|
||||
BITCOIND_RPC_USER="${BITCOIND_RPC_USER:-}" \
|
||||
BITCOIND_RPC_PASS="${BITCOIND_RPC_PASS:-}" \
|
||||
BTC_NETWORK="${BTC_NETWORK:-mainnet}" \
|
||||
BTC_CONFIRMATIONS_REQUIRED="${BTC_CONFIRMATIONS_REQUIRED:-6}" \
|
||||
BTC_INTAKE_SYNC_INTERVAL_MS="${BTC_INTAKE_SYNC_INTERVAL_MS:-30000}" \
|
||||
npm run start >"$LOG_DIR/btc-intake.log" 2>&1 &
|
||||
echo $! >"$LOG_DIR/btc-intake.pid"
|
||||
}
|
||||
|
||||
start_settlement
|
||||
sleep 4
|
||||
start_btc_intake
|
||||
sleep 3
|
||||
|
||||
API_KEY="${OMNL_API_KEY:-${OMNL_SUPER_ADMIN_SECURE_KEY:-}}"
|
||||
log "Smoke: GET /btc/wallet/status"
|
||||
curl -sf -H "Authorization: Bearer ${API_KEY}" \
|
||||
"http://127.0.0.1:3011/api/v1/settlement/btc/wallet/status" | head -c 500
|
||||
echo
|
||||
log "Smoke: GET btc-intake /health"
|
||||
curl -sf "http://127.0.0.1:3013/health" | head -c 200 || log "WARN: btc-intake degraded (bitcoind may be offline)"
|
||||
echo
|
||||
log "Wallet BTC production deploy complete"
|
||||
log " Deposit: POST http://127.0.0.1:3013/deposit-instructions"
|
||||
log " Settle: POST https://secure.omdnl.org/settlement/btc/wallet/l1-settle"
|
||||
log " Status: GET https://secure.omdnl.org/settlement/btc/wallet/status"
|
||||
@@ -172,6 +172,7 @@ start_svc() {
|
||||
OMNL_MINT_OPERATOR_PRIVATE_KEY="${OMNL_MINT_OPERATOR_PRIVATE_KEY:-}" \
|
||||
OMNL_REQUIRE_LIVE_BTC_PRICE="${OMNL_REQUIRE_LIVE_BTC_PRICE:-1}" \
|
||||
OMNL_BTC_LEDGER_CONFIG="${OMNL_BTC_LEDGER_CONFIG:-$REPO_DIR/config/btc-l1-ledger-1000.chain138.v1.json}" \
|
||||
OMNL_BTC_WALLET_CONFIG="${OMNL_BTC_WALLET_CONFIG:-$REPO_DIR/config/btc-wallet-l1.chain138.v1.json}" \
|
||||
CBTC_ADDRESS_138="${CBTC_ADDRESS_138:-}" \
|
||||
BTC_L1_RECIPIENT_ADDRESS="${BTC_L1_RECIPIENT_ADDRESS:-}" \
|
||||
BTC_EXISTING_MINT_HOLDER="${BTC_EXISTING_MINT_HOLDER:-}" \
|
||||
|
||||
@@ -26,7 +26,7 @@ set -u
|
||||
set +a
|
||||
|
||||
FRAG="$(mktemp)"
|
||||
grep -E '^(OMNL_FINERACT_|OMNL_PUBLIC_MONEY_SUPPLY=|OMNL_REQUIRE_API_KEY=|OMNL_CUSTOMER_SECURITY_PRODUCTION=|OMNL_CUSTOMER_API_KEYS=|OFFICE24_OPENING_M1_USD=|OMNL_PORTAL_INTERNAL_SECRET=|JWT_SECRET=|DATABASE_URL=|RPC_URL_138=|CHAIN_138_RPC_URL=|SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=|SETTLEMENT_ALLOW_HYBX_PRODUCTION=|OMNL_ALLOW_CHAIN_MINT_EXECUTE=|OMNL_MINT_OPERATOR_PRIVATE_KEY=|OMNL_BTC_LEDGER_CONFIG=|CBTC_ADDRESS_138=|BTC_L1_RECIPIENT_ADDRESS=|BTC_EXISTING_MINT_HOLDER=|DODO_PMM_INTEGRATION=|CHAIN_138_DODO_PMM_INTEGRATION=|RESERVE_SYSTEM=|ORACLE_PROXY_ADDRESS=|CHAIN138_POOL_CBTC_CUSDT=|CHAIN138_POOL_CBTC_CUSDC=|CHAIN138_POOL_CBTC_CXAUC=|CHAIN138_ORACLE_URL=|CHAIN138_ORACLE_PORT=|TOKEN_AGGREGATION_URL=|OMNL_REQUIRE_LIVE_BTC_PRICE=|KEEPER_PRIVATE_KEY=|PRICE_FEED_KEEPER_ADDRESS=|AGGREGATOR_ADDRESS=|SWIFT_LISTENER_URL=|HYBX_BASE_URL=|HYBX_API=|HYBX_ENVIRONMENT=|HYBX_API_KEY=|HYBX_CLIENT_ID=|HYBX_CLIENT_SECRET=|HYBX_WEBHOOK_SECRET=|HYBX_MIFOS_FINERACT_SIDECAR_URL=|HYBX_ALLOW_LAN_SIDECAR=|INFURA_PROJECT_ID=|INFURA_PROJECT_SECRET=)' "$ENV_FILE" >"$FRAG" || true
|
||||
grep -E '^(OMNL_FINERACT_|OMNL_PUBLIC_MONEY_SUPPLY=|OMNL_REQUIRE_API_KEY=|OMNL_CUSTOMER_SECURITY_PRODUCTION=|OMNL_CUSTOMER_API_KEYS=|OFFICE24_OPENING_M1_USD=|OMNL_PORTAL_INTERNAL_SECRET=|JWT_SECRET=|DATABASE_URL=|RPC_URL_138=|CHAIN_138_RPC_URL=|SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE=|SETTLEMENT_ALLOW_HYBX_PRODUCTION=|OMNL_ALLOW_CHAIN_MINT_EXECUTE=|OMNL_MINT_OPERATOR_PRIVATE_KEY=|OMNL_BTC_LEDGER_CONFIG=|OMNL_BTC_WALLET_CONFIG=|CBTC_ADDRESS_138=|BTC_L1_RECIPIENT_ADDRESS=|BTC_EXISTING_MINT_HOLDER=|DODO_PMM_INTEGRATION=|CHAIN_138_DODO_PMM_INTEGRATION=|RESERVE_SYSTEM=|ORACLE_PROXY_ADDRESS=|CHAIN138_POOL_CBTC_CUSDT=|CHAIN138_POOL_CBTC_CUSDC=|CHAIN138_POOL_CBTC_CXAUC=|BITCOIND_RPC_URL=|BITCOIND_RPC_USER=|BITCOIND_RPC_PASS=|BTC_NETWORK=|BTC_CONFIRMATIONS_REQUIRED=|BTC_INTAKE_PORT=|SETTLEMENT_MIDDLEWARE_URL=|CHAIN138_ORACLE_URL=|CHAIN138_ORACLE_PORT=|TOKEN_AGGREGATION_URL=|OMNL_REQUIRE_LIVE_BTC_PRICE=|KEEPER_PRIVATE_KEY=|PRICE_FEED_KEEPER_ADDRESS=|AGGREGATOR_ADDRESS=|SWIFT_LISTENER_URL=|HYBX_BASE_URL=|HYBX_API=|HYBX_ENVIRONMENT=|HYBX_API_KEY=|HYBX_CLIENT_ID=|HYBX_CLIENT_SECRET=|HYBX_WEBHOOK_SECRET=|HYBX_MIFOS_FINERACT_SIDECAR_URL=|HYBX_ALLOW_LAN_SIDECAR=|INFURA_PROJECT_ID=|INFURA_PROJECT_SECRET=)' "$ENV_FILE" >"$FRAG" || true
|
||||
|
||||
ADMIN_ENV_KEY="$(node -e "
|
||||
const fs = require('fs');
|
||||
|
||||
@@ -64,11 +64,12 @@ export class SettlementMintJobSink implements MintJobSink {
|
||||
amountSats: job.amountSats,
|
||||
confirmations: job.confirmations ?? Number(process.env.BTC_CONFIRMATIONS_REQUIRED || 6),
|
||||
recipientAddress: job.chain138VaultAddress,
|
||||
settlementSource: 'wallet' as const,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await axios.post(
|
||||
`${this.settlementBase}/api/v1/settlement/btc/l1-settle`,
|
||||
`${this.settlementBase}/api/v1/settlement/btc/wallet/l1-settle`,
|
||||
body,
|
||||
{ headers, timeout: 180000 },
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type {
|
||||
AuditPublisher,
|
||||
CustodyAdapter,
|
||||
@@ -13,7 +15,19 @@ import type {
|
||||
MintJob,
|
||||
} from './types';
|
||||
|
||||
const DEFAULT_CONFIRMATIONS_REQUIRED = 6;
|
||||
const DEFAULT_CONFIRMATIONS_REQUIRED = Number(process.env.BTC_CONFIRMATIONS_REQUIRED || 6);
|
||||
|
||||
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 instructionsPath(): string {
|
||||
return path.join(dataDir(), 'instructions.json');
|
||||
}
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
@@ -34,7 +48,27 @@ export class NativeBitcoinWatcher {
|
||||
private readonly mintSink: MintJobSink,
|
||||
private readonly auditPublisher: AuditPublisher,
|
||||
private readonly outstandingPolicy?: OutstandingPolicy,
|
||||
) {}
|
||||
) {
|
||||
this.loadInstructionsFromDisk();
|
||||
}
|
||||
|
||||
private persistInstructions(): void {
|
||||
const rows = Array.from(this.instructions.values());
|
||||
fs.writeFileSync(instructionsPath(), JSON.stringify(rows, null, 2));
|
||||
}
|
||||
|
||||
private loadInstructionsFromDisk(): void {
|
||||
const file = instructionsPath();
|
||||
if (!fs.existsSync(file)) return;
|
||||
try {
|
||||
const rows = JSON.parse(fs.readFileSync(file, 'utf8')) as DepositInstruction[];
|
||||
for (const row of rows) {
|
||||
if (row?.id) this.instructions.set(row.id, row);
|
||||
}
|
||||
} catch {
|
||||
// ignore corrupt store — address-map.json still maps deposits
|
||||
}
|
||||
}
|
||||
|
||||
async createDepositInstruction(input: {
|
||||
basketMandate: BasketMandateSnapshot;
|
||||
@@ -60,6 +94,7 @@ export class NativeBitcoinWatcher {
|
||||
};
|
||||
|
||||
this.instructions.set(instruction.id, instruction);
|
||||
this.persistInstructions();
|
||||
await this.publishAudit({
|
||||
id: randomUUID(),
|
||||
instructionId,
|
||||
@@ -130,6 +165,7 @@ export class NativeBitcoinWatcher {
|
||||
},
|
||||
createdAt: instruction.updatedAt,
|
||||
});
|
||||
this.persistInstructions();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,7 +174,7 @@ export class NativeBitcoinWatcher {
|
||||
id: randomUUID(),
|
||||
instructionId: instruction.id,
|
||||
category: 'confirmation',
|
||||
message: 'Deposit reached 6 confirmations and is eligible for minting',
|
||||
message: `Deposit reached ${instruction.confirmationsRequired} confirmations and is eligible for minting`,
|
||||
metadata: {
|
||||
txId: event.txId,
|
||||
confirmations: event.confirmations,
|
||||
@@ -212,6 +248,7 @@ export class NativeBitcoinWatcher {
|
||||
this.mintJobs.set(instruction.id, mintJob);
|
||||
instruction.status = 'minted';
|
||||
instruction.updatedAt = timestamp;
|
||||
this.persistInstructions();
|
||||
|
||||
await this.publishAudit({
|
||||
id: randomUUID(),
|
||||
@@ -235,6 +272,7 @@ export class NativeBitcoinWatcher {
|
||||
instruction.status = 'frozen';
|
||||
instruction.freezeReason = reason;
|
||||
instruction.updatedAt = nowIso();
|
||||
this.persistInstructions();
|
||||
|
||||
await this.publishAudit({
|
||||
id: randomUUID(),
|
||||
|
||||
@@ -16,6 +16,7 @@ const btc_l1_settlement_1 = require("../../workflows/btc-l1-settlement");
|
||||
const fineract_1 = require("../../adapters/fineract");
|
||||
const pricing_1 = require("../../adapters/pricing");
|
||||
const btc_ledger_config_1 = require("../../btc-ledger-config");
|
||||
const btc_wallet_config_1 = require("../../btc-wallet-config");
|
||||
const external_banks_hub_1 = require("../../adapters/external-banks-hub");
|
||||
const card_issuers_hub_1 = require("../../adapters/card-issuers-hub");
|
||||
const auth_1 = require("../../middleware/auth");
|
||||
@@ -588,6 +589,7 @@ function createSettlementRouter() {
|
||||
officeId: (0, config_1.settlementOfficeId)(body.officeId),
|
||||
confirmations: Number(body.confirmations ?? 0),
|
||||
amountSats: Number(body.amountSats),
|
||||
settlementSource: body.settlementSource ?? 'ledger',
|
||||
});
|
||||
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
|
||||
}
|
||||
@@ -648,6 +650,80 @@ function createSettlementRouter() {
|
||||
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
});
|
||||
/** Normal wallet L1 BTC → cBTC (bitcoind deposits via btc-intake). */
|
||||
router.post('/btc/wallet/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),
|
||||
settlementSource: 'wallet',
|
||||
});
|
||||
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
|
||||
}
|
||||
catch (e) {
|
||||
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
});
|
||||
router.get('/btc/wallet/status', async (_req, res) => {
|
||||
if (!(0, auth_1.requireApiKey)(_req, res))
|
||||
return;
|
||||
try {
|
||||
const walletCfg = (0, btc_wallet_config_1.loadBtcWalletConfig)();
|
||||
const [balancesHo, quote] = await Promise.all([
|
||||
(0, fineract_1.fetchGlBalances)(1),
|
||||
(0, pricing_1.resolveLiveBtcUsd)().catch((e) => ({
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})),
|
||||
]);
|
||||
const walletSettlements = settlement_store_1.settlementStore
|
||||
.list(500)
|
||||
.filter((r) => r.btcSettlement != null &&
|
||||
r.request.remittanceInfo?.includes('wallet settle'));
|
||||
const settledSats = walletSettlements
|
||||
.filter((r) => r.phase === 'SETTLED')
|
||||
.reduce((sum, r) => sum + (r.btcSettlement?.amountSats ?? 0), 0);
|
||||
res.json({
|
||||
...(0, btc_wallet_config_1.btcWalletStatusPayload)(),
|
||||
asOf: new Date().toISOString(),
|
||||
gl: {
|
||||
custodyHo12015: balancesHo['12015'] ?? '0',
|
||||
interofficeBtc12424: balancesHo['12424'] ?? '0',
|
||||
},
|
||||
btcUsd: 'btcUsdRate' in quote
|
||||
? { rate: quote.btcUsdRate, source: quote.source, asOf: quote.asOf }
|
||||
: { error: quote.error },
|
||||
mintQueue: {
|
||||
settledSats,
|
||||
settlementCount: walletSettlements.length,
|
||||
},
|
||||
capabilities: {
|
||||
depositInstructions: walletCfg.settlementApi?.depositInstructions,
|
||||
walletSettle: '/btc/wallet/l1-settle',
|
||||
backingMode: walletCfg.backingMode,
|
||||
note: 'Real L1 BTC from bitcoind wallet — custody-backed cBTC mint, not institutional ledger headroom',
|
||||
},
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
});
|
||||
router.get('/btc/ledger', async (_req, res) => {
|
||||
if (!(0, auth_1.requireApiKey)(_req, res))
|
||||
return;
|
||||
|
||||
62
services/settlement-middleware/dist/btc-wallet-config.js
vendored
Normal file
62
services/settlement-middleware/dist/btc-wallet-config.js
vendored
Normal file
@@ -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 });
|
||||
exports.loadBtcWalletConfig = loadBtcWalletConfig;
|
||||
exports.btcWalletStatusPayload = btcWalletStatusPayload;
|
||||
exports.clearBtcWalletConfigCache = clearBtcWalletConfigCache;
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const btc_ledger_config_1 = require("./btc-ledger-config");
|
||||
let cached = null;
|
||||
function repoRoot() {
|
||||
return path_1.default.resolve(__dirname, '../../../');
|
||||
}
|
||||
function configPath() {
|
||||
const rel = process.env.OMNL_BTC_WALLET_CONFIG ||
|
||||
process.env.BTC_WALLET_L1_CONFIG ||
|
||||
'config/btc-wallet-l1.chain138.v1.json';
|
||||
return path_1.default.isAbsolute(rel) ? rel : path_1.default.join(repoRoot(), rel.replace(/^\//, ''));
|
||||
}
|
||||
function loadBtcWalletConfig() {
|
||||
if (cached)
|
||||
return cached;
|
||||
const raw = JSON.parse(fs_1.default.readFileSync(configPath(), 'utf8'));
|
||||
const ledger = (0, btc_ledger_config_1.loadBtcLedgerConfig)();
|
||||
cached = {
|
||||
...raw,
|
||||
token: { ...ledger.token, ...raw.token },
|
||||
oracle: { ...ledger.oracle, ...raw.oracle },
|
||||
dodoEvm: { ...ledger.dodoEvm, ...raw.dodoEvm },
|
||||
pmmPools: { ...ledger.pmmPools, ...raw.pmmPools },
|
||||
quoteTokens: { ...ledger.quoteTokens, ...raw.quoteTokens },
|
||||
rpc: { ...ledger.rpc, ...raw.rpc },
|
||||
confirmationsRequired: Number(process.env.BTC_CONFIRMATIONS_REQUIRED || raw.confirmationsRequired || 6),
|
||||
maxDepositSats: Number(process.env.BTC_WALLET_MAX_DEPOSIT_SATS || raw.maxDepositSats || 0),
|
||||
};
|
||||
return cached;
|
||||
}
|
||||
function btcWalletStatusPayload() {
|
||||
const cfg = loadBtcWalletConfig();
|
||||
return {
|
||||
configPath: configPath(),
|
||||
settlementSource: 'wallet',
|
||||
chainId: cfg.chainId,
|
||||
officeId: cfg.officeId,
|
||||
confirmationsRequired: cfg.confirmationsRequired,
|
||||
maxDepositSats: cfg.maxDepositSats,
|
||||
backingMode: cfg.backingMode,
|
||||
token: cfg.token,
|
||||
recipients: cfg.recipients,
|
||||
custody: cfg.custody,
|
||||
dodoEvm: { pmmIntegration: cfg.dodoEvm.pmmIntegration },
|
||||
pmmPools: cfg.pmmPools,
|
||||
rpc: cfg.rpc,
|
||||
depositApi: cfg.settlementApi?.depositInstructions,
|
||||
settleEndpoint: cfg.settlementApi?.walletL1Settle ?? '/btc/wallet/l1-settle',
|
||||
};
|
||||
}
|
||||
function clearBtcWalletConfigCache() {
|
||||
cached = null;
|
||||
}
|
||||
@@ -15,8 +15,18 @@ const fineract_1 = require("../adapters/fineract");
|
||||
const omnl_1 = require("../adapters/omnl");
|
||||
const pricing_1 = require("../adapters/pricing");
|
||||
const btc_ledger_config_1 = require("../btc-ledger-config");
|
||||
const btc_wallet_config_1 = require("../btc-wallet-config");
|
||||
const settlement_store_1 = require("../store/settlement-store");
|
||||
const MIN_CONFIRMATIONS = Number(process.env.BTC_CONFIRMATIONS_REQUIRED || 6);
|
||||
function resolveSettlementSource(input) {
|
||||
const explicit = input.settlementSource?.trim();
|
||||
if (explicit === 'wallet' || explicit === 'ledger')
|
||||
return explicit;
|
||||
return 'wallet';
|
||||
}
|
||||
function loadTokenConfig(source) {
|
||||
return source === 'ledger' ? (0, btc_ledger_config_1.loadBtcLedgerConfig)() : (0, btc_wallet_config_1.loadBtcWalletConfig)();
|
||||
}
|
||||
/** Env SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE overrides production JSON when set. */
|
||||
function allowChainMintExecute() {
|
||||
const env = process.env.SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE?.trim();
|
||||
@@ -46,7 +56,10 @@ function formatSettlementError(err) {
|
||||
async function processBtcL1Settlement(input) {
|
||||
const profile = (0, config_1.loadOfficeProfile)();
|
||||
const officeId = (0, config_1.settlementOfficeId)(input.officeId);
|
||||
const req = { ...input, officeId };
|
||||
const settlementSource = resolveSettlementSource(input);
|
||||
const tokenCfg = loadTokenConfig(settlementSource);
|
||||
const walletCfg = settlementSource === 'wallet' ? (0, btc_wallet_config_1.loadBtcWalletConfig)() : null;
|
||||
const req = { ...input, officeId, settlementSource };
|
||||
if (!req.idempotencyKey || !req.txId || !req.instructionId) {
|
||||
throw new Error('idempotencyKey, txId, and instructionId are required');
|
||||
}
|
||||
@@ -89,7 +102,7 @@ async function processBtcL1Settlement(input) {
|
||||
amount: btcAmount,
|
||||
creditorIban: 'INTERNAL-BTC-L1-CUSTODY',
|
||||
beneficiaryName: req.recipientAddress,
|
||||
remittanceInfo: `L1 BTC settle tx=${req.txId} instruction=${req.instructionId} → cBTC`,
|
||||
remittanceInfo: `L1 BTC ${settlementSource} settle tx=${req.txId} instruction=${req.instructionId} → cBTC`,
|
||||
moneyLayers: ['M0', 'M2', 'M3'],
|
||||
rail: 'CHAIN138',
|
||||
convertToCrypto: {
|
||||
@@ -128,7 +141,7 @@ async function processBtcL1Settlement(input) {
|
||||
priceAsOf: quote.asOf,
|
||||
},
|
||||
verbiageDocument: [
|
||||
`OMNL L1 BTC custody settlement (IFRS 13 fair value)`,
|
||||
`OMNL L1 BTC ${settlementSource} settlement (IFRS 13 fair value)`,
|
||||
`Office ${officeId} · instruction ${req.instructionId}`,
|
||||
`L1 tx ${req.txId} · ${req.confirmations} confirmations`,
|
||||
`Deposit ${req.depositAddress}`,
|
||||
@@ -150,16 +163,31 @@ async function processBtcL1Settlement(input) {
|
||||
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;
|
||||
let loadAmount = '0.00';
|
||||
let fullyBacked = false;
|
||||
if (settlementSource === 'wallet') {
|
||||
const walletLoad = (0, settlement_core_1.walletBtcCustodyBackedLoad)(usdValue, walletCfg?.maxDepositSats, req.amountSats);
|
||||
loadAmount = walletLoad.loadAmount;
|
||||
fullyBacked = walletLoad.fullyBacked;
|
||||
if (!fullyBacked) {
|
||||
record.errors.push(walletLoad.reason ||
|
||||
`Wallet BTC custody-backed load rejected for ${req.amountSats} sats`);
|
||||
advance('FAILED', { fineractJournalRef: String(custodyJe.resourceId) });
|
||||
return record;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const balances = await (0, fineract_1.fetchGlBalances)(officeId);
|
||||
const snap = (0, settlement_core_1.buildMoneySupplySnapshot)(officeId, balances);
|
||||
const m2Check = (0, settlement_core_1.m2FiatToTokenLoadAmount)(usdValue, snap.M2.broadMoney);
|
||||
const m3Check = (0, settlement_core_1.m3TokenLoadAmount)(m2Check.loadAmount, snap.M2.broadMoney, snap.M3.tokenLiabilities);
|
||||
loadAmount = m2Check.loadAmount;
|
||||
fullyBacked = m2Check.fullyBacked && m3Check.fullyBacked;
|
||||
if (!fullyBacked) {
|
||||
record.errors.push(`M2/M3 backing insufficient for ledger BTC settlement (M2 load ${m2Check.loadAmount}/${usdValue}, M3 headroom ${m3Check.loadAmount})`);
|
||||
advance('FAILED', { fineractJournalRef: String(custodyJe.resourceId) });
|
||||
return record;
|
||||
}
|
||||
}
|
||||
const m2Amount = parseFloat(loadAmount) || 0;
|
||||
let m2m3Ref = '';
|
||||
@@ -172,7 +200,7 @@ async function processBtcL1Settlement(input) {
|
||||
officeId,
|
||||
transactionDate: valueDate,
|
||||
referenceNumber: `${req.idempotencyKey}-M2M3`,
|
||||
comments: `T-BTC-M2M3 cBTC load ${btcAmount} BTC`,
|
||||
comments: `T-BTC-${settlementSource.toUpperCase()}-M2M3 cBTC load ${btcAmount} BTC`,
|
||||
debits: [{ glAccountId: debitGlId, amount: m2Amount }],
|
||||
credits: [{ glAccountId: creditGlId, amount: m2Amount }],
|
||||
});
|
||||
@@ -184,15 +212,14 @@ async function processBtcL1Settlement(input) {
|
||||
advance('HYBX_RAIL_DISPATCHED');
|
||||
advance('ISO20022_ARCHIVED');
|
||||
// 3) Mint cBTC in satoshi-accurate BTC decimal amount
|
||||
const btcLedger = (0, btc_ledger_config_1.loadBtcLedgerConfig)();
|
||||
const mint = await (0, omnl_1.requestTokenMint)({
|
||||
lineId: btcLedger.token.lineId,
|
||||
lineId: tokenCfg.token.lineId,
|
||||
amount: btcAmount,
|
||||
recipient: req.recipientAddress,
|
||||
settlementRef: record.settlementId,
|
||||
dryRun: !allowChainMintExecute(),
|
||||
symbol: btcLedger.token.symbol,
|
||||
tokenAddress: btcLedger.token.address,
|
||||
symbol: tokenCfg.token.symbol,
|
||||
tokenAddress: tokenCfg.token.address,
|
||||
});
|
||||
advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash });
|
||||
advance('SETTLED');
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { fetchGlBalances, fineractOfficeReachable } from '../../adapters/fineract';
|
||||
import { resolveLiveBtcUsd } from '../../adapters/pricing';
|
||||
import { btcLedgerContractsPayload, loadBtcLedgerConfig } from '../../btc-ledger-config';
|
||||
import { btcWalletStatusPayload, loadBtcWalletConfig } from '../../btc-wallet-config';
|
||||
import {
|
||||
dispatchExternalBankOrExchange,
|
||||
getExternalRailsStatus,
|
||||
@@ -619,6 +620,7 @@ export function createSettlementRouter(): Router {
|
||||
officeId: settlementOfficeId(body.officeId),
|
||||
confirmations: Number(body.confirmations ?? 0),
|
||||
amountSats: Number(body.amountSats),
|
||||
settlementSource: body.settlementSource ?? 'ledger',
|
||||
});
|
||||
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
|
||||
} catch (e) {
|
||||
@@ -680,6 +682,86 @@ export function createSettlementRouter(): Router {
|
||||
}
|
||||
});
|
||||
|
||||
/** Normal wallet L1 BTC → cBTC (bitcoind deposits via btc-intake). */
|
||||
router.post('/btc/wallet/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),
|
||||
settlementSource: 'wallet',
|
||||
});
|
||||
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/btc/wallet/status', async (_req, res) => {
|
||||
if (!requireApiKey(_req, res)) return;
|
||||
try {
|
||||
const walletCfg = loadBtcWalletConfig();
|
||||
const [balancesHo, quote] = await Promise.all([
|
||||
fetchGlBalances(1),
|
||||
resolveLiveBtcUsd().catch((e) => ({
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})),
|
||||
]);
|
||||
const walletSettlements = settlementStore
|
||||
.list(500)
|
||||
.filter(
|
||||
(r) =>
|
||||
r.btcSettlement != null &&
|
||||
r.request.remittanceInfo?.includes('wallet settle'),
|
||||
);
|
||||
const settledSats = walletSettlements
|
||||
.filter((r) => r.phase === 'SETTLED')
|
||||
.reduce((sum, r) => sum + (r.btcSettlement?.amountSats ?? 0), 0);
|
||||
|
||||
res.json({
|
||||
...btcWalletStatusPayload(),
|
||||
asOf: new Date().toISOString(),
|
||||
gl: {
|
||||
custodyHo12015: balancesHo['12015'] ?? '0',
|
||||
interofficeBtc12424: balancesHo['12424'] ?? '0',
|
||||
},
|
||||
btcUsd:
|
||||
'btcUsdRate' in quote
|
||||
? { rate: quote.btcUsdRate, source: quote.source, asOf: quote.asOf }
|
||||
: { error: (quote as { error: string }).error },
|
||||
mintQueue: {
|
||||
settledSats,
|
||||
settlementCount: walletSettlements.length,
|
||||
},
|
||||
capabilities: {
|
||||
depositInstructions: walletCfg.settlementApi?.depositInstructions,
|
||||
walletSettle: '/btc/wallet/l1-settle',
|
||||
backingMode: walletCfg.backingMode,
|
||||
note: 'Real L1 BTC from bitcoind wallet — custody-backed cBTC mint, not institutional ledger headroom',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/btc/ledger', async (_req, res) => {
|
||||
if (!requireApiKey(_req, res)) return;
|
||||
try {
|
||||
|
||||
72
services/settlement-middleware/src/btc-wallet-config.ts
Normal file
72
services/settlement-middleware/src/btc-wallet-config.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type { BtcLedgerConfig } from './btc-ledger-config';
|
||||
import { loadBtcLedgerConfig } from './btc-ledger-config';
|
||||
|
||||
export type BtcWalletConfig = BtcLedgerConfig & {
|
||||
settlementSource: 'wallet';
|
||||
confirmationsRequired: number;
|
||||
maxDepositSats: number;
|
||||
backingMode: 'custody_fair_value';
|
||||
custody: { debitGl: string; creditGl: string; memo: string };
|
||||
recipients: BtcLedgerConfig['recipients'] & { defaultVault?: string };
|
||||
};
|
||||
|
||||
let cached: BtcWalletConfig | null = null;
|
||||
|
||||
function repoRoot(): string {
|
||||
return path.resolve(__dirname, '../../../');
|
||||
}
|
||||
|
||||
function configPath(): string {
|
||||
const rel =
|
||||
process.env.OMNL_BTC_WALLET_CONFIG ||
|
||||
process.env.BTC_WALLET_L1_CONFIG ||
|
||||
'config/btc-wallet-l1.chain138.v1.json';
|
||||
return path.isAbsolute(rel) ? rel : path.join(repoRoot(), rel.replace(/^\//, ''));
|
||||
}
|
||||
|
||||
export function loadBtcWalletConfig(): BtcWalletConfig {
|
||||
if (cached) return cached;
|
||||
const raw = JSON.parse(fs.readFileSync(configPath(), 'utf8')) as BtcWalletConfig;
|
||||
const ledger = loadBtcLedgerConfig();
|
||||
cached = {
|
||||
...raw,
|
||||
token: { ...ledger.token, ...raw.token },
|
||||
oracle: { ...ledger.oracle, ...raw.oracle },
|
||||
dodoEvm: { ...ledger.dodoEvm, ...raw.dodoEvm },
|
||||
pmmPools: { ...ledger.pmmPools, ...raw.pmmPools },
|
||||
quoteTokens: { ...ledger.quoteTokens, ...raw.quoteTokens },
|
||||
rpc: { ...ledger.rpc, ...raw.rpc },
|
||||
confirmationsRequired: Number(
|
||||
process.env.BTC_CONFIRMATIONS_REQUIRED || raw.confirmationsRequired || 6,
|
||||
),
|
||||
maxDepositSats: Number(process.env.BTC_WALLET_MAX_DEPOSIT_SATS || raw.maxDepositSats || 0),
|
||||
};
|
||||
return cached;
|
||||
}
|
||||
|
||||
export function btcWalletStatusPayload(): Record<string, unknown> {
|
||||
const cfg = loadBtcWalletConfig();
|
||||
return {
|
||||
configPath: configPath(),
|
||||
settlementSource: 'wallet',
|
||||
chainId: cfg.chainId,
|
||||
officeId: cfg.officeId,
|
||||
confirmationsRequired: cfg.confirmationsRequired,
|
||||
maxDepositSats: cfg.maxDepositSats,
|
||||
backingMode: cfg.backingMode,
|
||||
token: cfg.token,
|
||||
recipients: cfg.recipients,
|
||||
custody: cfg.custody,
|
||||
dodoEvm: { pmmIntegration: cfg.dodoEvm.pmmIntegration },
|
||||
pmmPools: cfg.pmmPools,
|
||||
rpc: cfg.rpc,
|
||||
depositApi: cfg.settlementApi?.depositInstructions,
|
||||
settleEndpoint: cfg.settlementApi?.walletL1Settle ?? '/btc/wallet/l1-settle',
|
||||
};
|
||||
}
|
||||
|
||||
export function clearBtcWalletConfigCache(): void {
|
||||
cached = null;
|
||||
}
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
m3TokenLoadAmount,
|
||||
resolveBtcL1CustodyJournal,
|
||||
satsToBtcDecimal,
|
||||
walletBtcCustodyBackedLoad,
|
||||
type BtcL1ReconcileRequest,
|
||||
type BtcL1SettlementRequest,
|
||||
type BtcL1SettlementSource,
|
||||
type SettlementRecord,
|
||||
} from '@dbis/settlement-core';
|
||||
import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config';
|
||||
@@ -22,10 +24,21 @@ import {
|
||||
import { requestTokenMint } from '../adapters/omnl';
|
||||
import { resolveLiveBtcUsd } from '../adapters/pricing';
|
||||
import { loadBtcLedgerConfig } from '../btc-ledger-config';
|
||||
import { loadBtcWalletConfig } from '../btc-wallet-config';
|
||||
import { settlementStore } from '../store/settlement-store';
|
||||
|
||||
const MIN_CONFIRMATIONS = Number(process.env.BTC_CONFIRMATIONS_REQUIRED || 6);
|
||||
|
||||
function resolveSettlementSource(input: BtcL1SettlementRequest): BtcL1SettlementSource {
|
||||
const explicit = input.settlementSource?.trim();
|
||||
if (explicit === 'wallet' || explicit === 'ledger') return explicit;
|
||||
return 'wallet';
|
||||
}
|
||||
|
||||
function loadTokenConfig(source: BtcL1SettlementSource) {
|
||||
return source === 'ledger' ? loadBtcLedgerConfig() : loadBtcWalletConfig();
|
||||
}
|
||||
|
||||
/** 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();
|
||||
@@ -58,7 +71,10 @@ export async function processBtcL1Settlement(
|
||||
): Promise<SettlementRecord> {
|
||||
const profile = loadOfficeProfile();
|
||||
const officeId = settlementOfficeId(input.officeId);
|
||||
const req = { ...input, officeId };
|
||||
const settlementSource = resolveSettlementSource(input);
|
||||
const tokenCfg = loadTokenConfig(settlementSource);
|
||||
const walletCfg = settlementSource === 'wallet' ? loadBtcWalletConfig() : null;
|
||||
const req = { ...input, officeId, settlementSource };
|
||||
|
||||
if (!req.idempotencyKey || !req.txId || !req.instructionId) {
|
||||
throw new Error('idempotencyKey, txId, and instructionId are required');
|
||||
@@ -109,7 +125,7 @@ export async function processBtcL1Settlement(
|
||||
amount: btcAmount,
|
||||
creditorIban: 'INTERNAL-BTC-L1-CUSTODY',
|
||||
beneficiaryName: req.recipientAddress,
|
||||
remittanceInfo: `L1 BTC settle tx=${req.txId} instruction=${req.instructionId} → cBTC`,
|
||||
remittanceInfo: `L1 BTC ${settlementSource} settle tx=${req.txId} instruction=${req.instructionId} → cBTC`,
|
||||
moneyLayers: ['M0', 'M2', 'M3'],
|
||||
rail: 'CHAIN138',
|
||||
convertToCrypto: {
|
||||
@@ -152,7 +168,7 @@ export async function processBtcL1Settlement(
|
||||
priceAsOf: quote.asOf,
|
||||
},
|
||||
verbiageDocument: [
|
||||
`OMNL L1 BTC custody settlement (IFRS 13 fair value)`,
|
||||
`OMNL L1 BTC ${settlementSource} settlement (IFRS 13 fair value)`,
|
||||
`Office ${officeId} · instruction ${req.instructionId}`,
|
||||
`L1 tx ${req.txId} · ${req.confirmations} confirmations`,
|
||||
`Deposit ${req.depositAddress}`,
|
||||
@@ -176,18 +192,39 @@ export async function processBtcL1Settlement(
|
||||
});
|
||||
|
||||
// 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})`,
|
||||
let loadAmount = '0.00';
|
||||
let fullyBacked = false;
|
||||
|
||||
if (settlementSource === 'wallet') {
|
||||
const walletLoad = walletBtcCustodyBackedLoad(
|
||||
usdValue,
|
||||
walletCfg?.maxDepositSats,
|
||||
req.amountSats,
|
||||
);
|
||||
advance('FAILED', {
|
||||
fineractJournalRef: String(custodyJe.resourceId),
|
||||
});
|
||||
return record;
|
||||
loadAmount = walletLoad.loadAmount;
|
||||
fullyBacked = walletLoad.fullyBacked;
|
||||
if (!fullyBacked) {
|
||||
record.errors.push(
|
||||
walletLoad.reason ||
|
||||
`Wallet BTC custody-backed load rejected for ${req.amountSats} sats`,
|
||||
);
|
||||
advance('FAILED', { fineractJournalRef: String(custodyJe.resourceId) });
|
||||
return record;
|
||||
}
|
||||
} else {
|
||||
const balances = await fetchGlBalances(officeId);
|
||||
const snap = buildMoneySupplySnapshot(officeId, balances);
|
||||
const m2Check = m2FiatToTokenLoadAmount(usdValue, snap.M2.broadMoney);
|
||||
const m3Check = m3TokenLoadAmount(m2Check.loadAmount, snap.M2.broadMoney, snap.M3.tokenLiabilities);
|
||||
loadAmount = m2Check.loadAmount;
|
||||
fullyBacked = m2Check.fullyBacked && m3Check.fullyBacked;
|
||||
if (!fullyBacked) {
|
||||
record.errors.push(
|
||||
`M2/M3 backing insufficient for ledger BTC settlement (M2 load ${m2Check.loadAmount}/${usdValue}, M3 headroom ${m3Check.loadAmount})`,
|
||||
);
|
||||
advance('FAILED', { fineractJournalRef: String(custodyJe.resourceId) });
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
||||
const m2Amount = parseFloat(loadAmount) || 0;
|
||||
@@ -201,7 +238,7 @@ export async function processBtcL1Settlement(
|
||||
officeId,
|
||||
transactionDate: valueDate,
|
||||
referenceNumber: `${req.idempotencyKey}-M2M3`,
|
||||
comments: `T-BTC-M2M3 cBTC load ${btcAmount} BTC`,
|
||||
comments: `T-BTC-${settlementSource.toUpperCase()}-M2M3 cBTC load ${btcAmount} BTC`,
|
||||
debits: [{ glAccountId: debitGlId, amount: m2Amount }],
|
||||
credits: [{ glAccountId: creditGlId, amount: m2Amount }],
|
||||
});
|
||||
@@ -215,15 +252,14 @@ export async function processBtcL1Settlement(
|
||||
advance('ISO20022_ARCHIVED');
|
||||
|
||||
// 3) Mint cBTC in satoshi-accurate BTC decimal amount
|
||||
const btcLedger = loadBtcLedgerConfig();
|
||||
const mint = await requestTokenMint({
|
||||
lineId: btcLedger.token.lineId,
|
||||
lineId: tokenCfg.token.lineId,
|
||||
amount: btcAmount,
|
||||
recipient: req.recipientAddress,
|
||||
settlementRef: record.settlementId,
|
||||
dryRun: !allowChainMintExecute(),
|
||||
symbol: btcLedger.token.symbol,
|
||||
tokenAddress: btcLedger.token.address,
|
||||
symbol: tokenCfg.token.symbol,
|
||||
tokenAddress: tokenCfg.token.address,
|
||||
});
|
||||
advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash });
|
||||
advance('SETTLED');
|
||||
|
||||
Reference in New Issue
Block a user