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);
|
||||
120
scripts/deployment/btc-l1-settlement-e2e.mjs
Normal file
120
scripts/deployment/btc-l1-settlement-e2e.mjs
Normal file
@@ -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);
|
||||
});
|
||||
93
scripts/deployment/btc-regtest-bootstrap.mjs
Normal file
93
scripts/deployment/btc-regtest-bootstrap.mjs
Normal file
@@ -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,
|
||||
}),
|
||||
);
|
||||
@@ -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"
|
||||
|
||||
90
scripts/deployment/ensure-cbtc-pmm-pools.mjs
Normal file
90
scripts/deployment/ensure-cbtc-pmm-pools.mjs
Normal file
@@ -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));
|
||||
151
scripts/deployment/hot-cbtc-liquidity-1000.mjs
Normal file
151
scripts/deployment/hot-cbtc-liquidity-1000.mjs
Normal file
@@ -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);
|
||||
});
|
||||
109
scripts/deployment/seed-btc-custody-gl-accounts.mjs
Normal file
109
scripts/deployment/seed-btc-custody-gl-accounts.mjs
Normal file
@@ -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);
|
||||
}
|
||||
58
scripts/deployment/smoke-btc-settlement.mjs
Normal file
58
scripts/deployment/smoke-btc-settlement.mjs
Normal file
@@ -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);
|
||||
64
scripts/deployment/verify-cbtc-trade.mjs
Normal file
64
scripts/deployment/verify-cbtc-trade.mjs
Normal file
@@ -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));
|
||||
Reference in New Issue
Block a user