Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m3s
CI/CD Pipeline / Security Scanning (push) Successful in 2m58s
CI/CD Pipeline / Lint and Format (push) Failing after 46s
CI/CD Pipeline / Terraform Validation (push) Failing after 26s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 29s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 41s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 22s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 24s
Validation / validate-genesis (push) Successful in 32s
Validation / validate-terraform (push) Failing after 28s
Validation / validate-kubernetes (push) Failing after 13s
Validation / validate-smart-contracts (push) Failing after 12s
Validation / validate-security (push) Failing after 1m31s
Validation / validate-documentation (push) Failing after 23s
Verify Deployment / Verify Deployment (push) Failing after 1m4s
Add BTC L1 settle/reconcile/ledger APIs, bitcoind intake, cBTC PMM hot-LP scripts, and custody credential smoke tests (secrets stay gitignored). Enables full-prod local green health and server pull-deploy for secure.omdnl.org /btc/*. Co-authored-by: Cursor <cursoragent@cursor.com>
143 lines
4.1 KiB
TypeScript
143 lines
4.1 KiB
TypeScript
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<void> {
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
JSON.stringify({
|
|
level: 'audit',
|
|
service: 'btc-intake',
|
|
...record,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
class EnvOutstandingPolicy implements OutstandingPolicy {
|
|
async getCurrentOutstandingSats(): Promise<number> {
|
|
return Number(process.env.BTC_CURRENT_OUTSTANDING_SATS || 0);
|
|
}
|
|
|
|
async getMaxOutstandingSats(): Promise<number> {
|
|
return Number(process.env.BTC_MAX_OUTSTANDING_SATS || 0);
|
|
}
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
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);
|
|
});
|