feat(settlement): production Revolut, Wise, and Binance external rails
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 59s
CI/CD Pipeline / Security Scanning (push) Successful in 2m34s
CI/CD Pipeline / Lint and Format (push) Failing after 49s
CI/CD Pipeline / Terraform Validation (push) Failing after 22s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 26s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 19s
Validation / validate-genesis (push) Successful in 31s
Validation / validate-terraform (push) Failing after 27s
Validation / validate-kubernetes (push) Failing after 10s
Validation / validate-smart-contracts (push) Failing after 12s
Validation / validate-security (push) Failing after 1m26s
Validation / validate-documentation (push) Failing after 18s

Wire Business/Platform/Spot adapters with fail-closed live gates, status/probe/dispatch APIs, smoke script, and portal rails UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-19 12:14:37 -07:00
parent 9ea42d4df6
commit 4048112461
25 changed files with 3575 additions and 61 deletions

View File

@@ -0,0 +1,147 @@
#!/usr/bin/env node
/**
* Smoke-test Revolut Business + Wise + Binance credentials.
* Loads secrets/{revolut,wise,binance}/credentials.env then process.env.
* Usage: node scripts/rails/smoke-revolut-wise-binance.mjs
*/
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import { fileURLToPath } from 'url';
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)) return {};
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().replace(/^["']|["']$/g, '');
}
return out;
}
function applyEnv(obj) {
for (const [k, v] of Object.entries(obj)) {
if (v && (!(k in process.env) || !process.env[k])) process.env[k] = v;
}
}
function redact(s) {
if (!s) return '(empty)';
if (s.length <= 10) return '***';
return `${s.slice(0, 4)}${s.slice(-4)} (len=${s.length})`;
}
applyEnv(loadEnvFile('.env'));
applyEnv(loadEnvFile('secrets/revolut/credentials.env'));
applyEnv(loadEnvFile('secrets/wise/credentials.env'));
applyEnv(loadEnvFile('secrets/binance/credentials.env'));
async function smokeRevolut() {
const token = process.env.REVOLUT_ACCESS_TOKEN || process.env.REVOLUT_API_TOKEN;
const base = (process.env.REVOLUT_API_BASE_URL || 'https://b2b.revolut.com/api/1.0').replace(/\/$/, '');
if (!token) return { ok: false, configured: false, error: 'REVOLUT_ACCESS_TOKEN missing' };
const res = await fetch(`${base}/accounts`, {
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, 160) };
}
const accounts = Array.isArray(body) ? body : body?.accounts ?? [];
return {
ok: res.status >= 200 && res.status < 300,
configured: true,
http: res.status,
token: redact(token),
accountCount: Array.isArray(accounts) ? accounts.length : null,
accountIdSet: Boolean(process.env.REVOLUT_ACCOUNT_ID),
liveGate: process.env.SETTLEMENT_ALLOW_REVOLUT_PRODUCTION === '1',
error: res.ok ? undefined : body?.message || text.slice(0, 180),
};
}
async function smokeWise() {
const token = process.env.WISE_API_TOKEN || process.env.TRANSFERWISE_API_TOKEN;
const base = (process.env.WISE_API_BASE_URL || 'https://api.transferwise.com').replace(/\/$/, '');
if (!token) return { ok: false, configured: false, error: 'WISE_API_TOKEN missing' };
const res = await fetch(`${base}/v1/profiles`, {
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, 160) };
}
const profiles = Array.isArray(body) ? body : [];
return {
ok: res.status >= 200 && res.status < 300,
configured: true,
http: res.status,
token: redact(token),
profileCount: profiles.length,
profileIdSet: Boolean(process.env.WISE_PROFILE_ID),
liveGate: process.env.SETTLEMENT_ALLOW_WISE_PRODUCTION === '1',
error: res.ok ? undefined : body?.error || body?.message || text.slice(0, 180),
};
}
async function smokeBinance() {
const key = process.env.BINANCE_API_KEY;
const secret = process.env.BINANCE_API_SECRET;
const base = (process.env.BINANCE_API_BASE_URL || 'https://api.binance.com').replace(/\/$/, '');
if (!key || !secret) return { ok: false, configured: false, error: 'BINANCE_API_KEY/SECRET missing' };
const ping = await fetch(`${base}/api/v3/ping`);
const timestamp = Date.now();
const qs = `timestamp=${timestamp}`;
const sig = crypto.createHmac('sha256', secret).update(qs).digest('hex');
const res = await fetch(`${base}/api/v3/account?${qs}&signature=${sig}`, {
headers: { 'X-MBX-APIKEY': key, Accept: 'application/json' },
});
const text = await res.text();
let body;
try {
body = JSON.parse(text);
} catch {
body = { raw: text.slice(0, 160) };
}
const funded = (body.balances || []).filter((b) => Number(b.free) > 0).length;
return {
ok: ping.ok && res.status >= 200 && res.status < 300,
configured: true,
http: res.status,
ping: ping.status,
apiKey: redact(key),
fundedAssets: funded,
canTrade: body.canTrade,
liveGate: process.env.SETTLEMENT_ALLOW_BINANCE_PRODUCTION === '1',
withdrawGate: process.env.BINANCE_ALLOW_WITHDRAW === '1',
error: res.ok ? undefined : body?.msg || text.slice(0, 180),
};
}
const report = {
asOf: new Date().toISOString(),
revolut: await smokeRevolut(),
wise: await smokeWise(),
binance: await smokeBinance(),
};
console.log(JSON.stringify(report, null, 2));
const anyConfigured = report.revolut.configured || report.wise.configured || report.binance.configured;
const allOk =
(!report.revolut.configured || report.revolut.ok) &&
(!report.wise.configured || report.wise.ok) &&
(!report.binance.configured || report.binance.ok);
process.exit(anyConfigured && allOk ? 0 : anyConfigured ? 2 : 3);