feat(settlement): production BTC L1 rails, hot cBTC liquidity, Dfns/Cobo smoke
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m3s
CI/CD Pipeline / Security Scanning (push) Successful in 2m58s
CI/CD Pipeline / Lint and Format (push) Failing after 46s
CI/CD Pipeline / Terraform Validation (push) Failing after 26s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 29s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 41s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 22s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 24s
Validation / validate-genesis (push) Successful in 32s
Validation / validate-terraform (push) Failing after 28s
Validation / validate-kubernetes (push) Failing after 13s
Validation / validate-smart-contracts (push) Failing after 12s
Validation / validate-security (push) Failing after 1m31s
Validation / validate-documentation (push) Failing after 23s
Verify Deployment / Verify Deployment (push) Failing after 1m4s

Add BTC L1 settle/reconcile/ledger APIs, bitcoind intake, cBTC PMM hot-LP scripts, and custody credential smoke tests (secrets stay gitignored). Enables full-prod local green health and server pull-deploy for secure.omdnl.org /btc/*.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-19 09:26:45 -07:00
parent 286b2a8915
commit 5a9889cace
68 changed files with 7558 additions and 16 deletions

21
scripts/custody/package-lock.json generated Normal file
View File

@@ -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/"
}
}
}
}

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"@noble/ed25519": "^3.1.0"
}
}

View File

@@ -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);