feat(settlement): production BTC L1 rails, hot cBTC liquidity, Dfns/Cobo smoke
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m3s
CI/CD Pipeline / Security Scanning (push) Successful in 2m58s
CI/CD Pipeline / Lint and Format (push) Failing after 46s
CI/CD Pipeline / Terraform Validation (push) Failing after 26s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 29s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 41s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 22s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 24s
Validation / validate-genesis (push) Successful in 32s
Validation / validate-terraform (push) Failing after 28s
Validation / validate-kubernetes (push) Failing after 13s
Validation / validate-smart-contracts (push) Failing after 12s
Validation / validate-security (push) Failing after 1m31s
Validation / validate-documentation (push) Failing after 23s
Verify Deployment / Verify Deployment (push) Failing after 1m4s
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m3s
CI/CD Pipeline / Security Scanning (push) Successful in 2m58s
CI/CD Pipeline / Lint and Format (push) Failing after 46s
CI/CD Pipeline / Terraform Validation (push) Failing after 26s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 29s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 41s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 22s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 24s
Validation / validate-genesis (push) Successful in 32s
Validation / validate-terraform (push) Failing after 28s
Validation / validate-kubernetes (push) Failing after 13s
Validation / validate-smart-contracts (push) Failing after 12s
Validation / validate-security (push) Failing after 1m31s
Validation / validate-documentation (push) Failing after 23s
Verify Deployment / Verify Deployment (push) Failing after 1m4s
Add BTC L1 settle/reconcile/ledger APIs, bitcoind intake, cBTC PMM hot-LP scripts, and custody credential smoke tests (secrets stay gitignored). Enables full-prod local green health and server pull-deploy for secure.omdnl.org /btc/*. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
21
scripts/custody/package-lock.json
generated
Normal file
21
scripts/custody/package-lock.json
generated
Normal 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/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
scripts/custody/package.json
Normal file
5
scripts/custody/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@noble/ed25519": "^3.1.0"
|
||||
}
|
||||
}
|
||||
234
scripts/custody/smoke-dfns-cobo.mjs
Normal file
234
scripts/custody/smoke-dfns-cobo.mjs
Normal 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);
|
||||
Reference in New Issue
Block a user