#!/usr/bin/env node import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { createConnection } from 'node:net'; import { basename, dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { Command } from 'commander'; const require = createRequire(import.meta.url); const scriptDirectory = dirname(fileURLToPath(import.meta.url)); const topologyFile = resolve(scriptDirectory, 'universa.json'); const universaCoreDist = resolve(scriptDirectory, '../node_modules/universa-core/dist'); const networkTimeoutMs = 20000; const endpointProbeTimeoutMs = 3000; function withTimeout(promise, timeoutMs, message) { let timeoutId; const timeout = new Promise((_, reject) => { timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs); }); return Promise.race([promise, timeout]).finally(() => clearTimeout(timeoutId)); } async function withUniversaWasmLoader(callback) { const fetch = globalThis.fetch; const read = globalThis.read; const readBinary = globalThis.readBinary; globalThis.fetch = undefined; globalThis.read = (fileName, binary) => { const wasmPath = resolve(universaCoreDist, basename(fileName)); const data = readFileSync(wasmPath); return binary ? data : data.toString('binary'); }; globalThis.readBinary = (fileName) => { const wasmPath = resolve(universaCoreDist, basename(fileName)); return readFileSync(wasmPath); }; try { return await callback(); } finally { globalThis.fetch = fetch; if (read === undefined) delete globalThis.read; else globalThis.read = read; if (readBinary === undefined) delete globalThis.readBinary; else globalThis.readBinary = readBinary; } } function loadUniversaCore() { const windowDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'window'); const navigatorDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'navigator'); const localStorageDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'localStorage'); Object.defineProperty(globalThis, 'window', { configurable: true, value: globalThis }); Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { hardwareConcurrency: 1, languages: ['en-US'] } }); Object.defineProperty(globalThis, 'localStorage', { configurable: true, value: { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {} } }); try { return require('universa-core'); } finally { if (windowDescriptor) Object.defineProperty(globalThis, 'window', windowDescriptor); else delete globalThis.window; if (navigatorDescriptor) Object.defineProperty(globalThis, 'navigator', navigatorDescriptor); else delete globalThis.navigator; if (localStorageDescriptor) Object.defineProperty(globalThis, 'localStorage', localStorageDescriptor); else delete globalThis.localStorage; } } function generatePrivateKey() { return withUniversaWasmLoader(async () => { const { PrivateKey } = require('unicrypto'); return PrivateKey.generate({ strength: 2048 }); }); } async function loadTopology(Topology) { return withUniversaWasmLoader(async () => { const packedTopology = readTopology(); return Topology.load(packedTopology); }); } function readTopology() { const parsedTopology = JSON.parse(readFileSync(topologyFile, 'utf8')); const packedTopology = Array.isArray(parsedTopology) ? { list: parsedTopology, updated: Math.floor(Date.now() / 1000) } : parsedTopology; if (!Array.isArray(packedTopology.list)) { throw new Error(`Topology file must contain a node array or an object with a list array: ${topologyFile}`); } return packedTopology; } async function probeEndpoint(url) { return new Promise((resolveProbe) => { const endpoint = new URL(url); const port = endpoint.port || (endpoint.protocol === 'https:' ? 443 : 80); const socket = createConnection({ host: endpoint.hostname, port: Number(port), timeout: endpointProbeTimeoutMs }); function finish(ok, error = '') { socket.destroy(); resolveProbe({ url, ok, error }); } socket.once('connect', () => finish(true)); socket.once('timeout', () => finish(false, 'TCP connection timed out')); socket.once('error', (error) => finish(false, error.message)); }); } async function assertReachableTopology(directConnection) { const topology = readTopology(); const urls = topology.list.flatMap((node) => ( directConnection ? node.direct_urls : node.domain_urls ) || []); if (urls.length === 0) { throw new Error(`Topology file does not contain ${directConnection ? 'direct_urls' : 'domain_urls'} entries: ${topologyFile}`); } const results = await Promise.all(urls.map(probeEndpoint)); if (results.some((result) => result.ok)) return; const details = results .map(({ url, error }) => ` ${url}/topology: ${error}`) .join('\n'); throw new Error(`No reachable topology endpoints in ${topologyFile}:\n${details}`); } const program = new Command(); program .name('u-js-sample') .description('Example Node.js console script with universa-core functions.') .option('--generate-key', 'generate a Universa private key and print its short address') .option('--test-network', `generate a key and connect to the Universa network from ${basename(topologyFile)}`) .action(async (options) => { if (options.generateKey || options.testNetwork) { const privateKey = await generatePrivateKey(); console.log(`Universa short address: ${privateKey.publicKey.shortAddress58}`); if (options.testNetwork) { const { Network, Topology } = loadUniversaCore(); const topology = await loadTopology(Topology); const directConnection = true; const network = new Network(privateKey, { topology, directConnection }); console.log(`Connecting with topology: ${topologyFile}`); await assertReachableTopology(directConnection); await withTimeout(network.connect(), networkTimeoutMs, 'Network connection timed out'); console.log(`Connected to ${network.size()} nodes`); const response = await withTimeout(network.command('sping'), 10000, 'Network sping command timed out'); console.log(`sping response: ${JSON.stringify(response)}`); } } }); try { await program.parseAsync(); } catch (error) { console.error(error instanceof Error ? error.message : error); process.exitCode = 1; } finally { process.exit(); }