Simplify sample with universa-core2 API
This commit is contained in:
1 parent
d84d085d8c
commit
e24914e76a
6 files changed
+82
-379
No files matched your search
+70
-166
@@ -1,199 +1,103 @@
|
||||
#!/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 { readFile } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Command } from 'commander';
|
||||
import {
|
||||
Network,
|
||||
PrivateKey,
|
||||
Topology,
|
||||
unicryptoReady
|
||||
} from 'universa-core2';
|
||||
|
||||
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;
|
||||
const sourceDirectory = dirname(fileURLToPath(import.meta.url));
|
||||
const topologyFile = resolve(sourceDirectory, 'universa.json');
|
||||
|
||||
function withTimeout(promise, timeoutMs, message) {
|
||||
async function withTimeout(promise, milliseconds, 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: () => {}
|
||||
}
|
||||
timeoutId = setTimeout(() => reject(new Error(message)), milliseconds);
|
||||
});
|
||||
|
||||
try {
|
||||
return require('universa-core');
|
||||
return await Promise.race([promise, timeout]);
|
||||
} 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;
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
function generatePrivateKey() {
|
||||
return withUniversaWasmLoader(async () => {
|
||||
const { PrivateKey } = require('unicrypto');
|
||||
return PrivateKey.generate({ strength: 2048 });
|
||||
async function loadTopology() {
|
||||
const packed = JSON.parse(await readFile(topologyFile, 'utf8'));
|
||||
return Topology.load(packed);
|
||||
}
|
||||
|
||||
async function testNetwork(privateKey) {
|
||||
const topology = await loadTopology();
|
||||
const network = new Network(privateKey, {
|
||||
topology,
|
||||
directConnection: true
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTopology(Topology) {
|
||||
return withUniversaWasmLoader(async () => {
|
||||
const packedTopology = readTopology();
|
||||
return Topology.load(packedTopology);
|
||||
});
|
||||
}
|
||||
console.log(`Testing ${topology.size()} Universa nodes from ${topologyFile}`);
|
||||
await withTimeout(network.connect(), 15_000, 'Network connection timed out');
|
||||
|
||||
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 });
|
||||
const nodes = Object.entries(network.topology.nodes);
|
||||
const results = await Promise.all(nodes.map(async ([id, node]) => {
|
||||
try {
|
||||
const connection = await withTimeout(
|
||||
network.nodeConnection(id),
|
||||
10_000,
|
||||
`${node.name} connection timed out`
|
||||
);
|
||||
const response = await connection.command('sping', {}, { timeout: 5_000 });
|
||||
if (response?.sping !== 'spong') throw new Error('unexpected sping response');
|
||||
console.log(`PASS ${node.name}: authenticated, sping=spong`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`FAIL ${node.name}: ${error instanceof Error ? error.message : error}`);
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
|
||||
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 passed = results.filter(Boolean).length;
|
||||
const quorum = Math.max(1, Math.ceil(nodes.length * 0.4));
|
||||
if (passed < quorum) {
|
||||
throw new Error(`Network quorum failed: ${passed}/${nodes.length} nodes passed; ${quorum} required`);
|
||||
}
|
||||
|
||||
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}`);
|
||||
console.log(`Universa network is functioning: ${passed}/${nodes.length} nodes passed`);
|
||||
}
|
||||
|
||||
const program = new Command();
|
||||
function printHelp() {
|
||||
console.log(`Usage: u-js-sample [options]
|
||||
|
||||
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}`);
|
||||
Options:
|
||||
--generate-key Generate a private key and print its short address
|
||||
--test-network Test every node in src/universa.json
|
||||
--help Show this help`);
|
||||
}
|
||||
|
||||
if (options.testNetwork) {
|
||||
const { Network, Topology } = loadUniversaCore();
|
||||
const topology = await loadTopology(Topology);
|
||||
const directConnection = true;
|
||||
const network = new Network(privateKey, { topology, directConnection });
|
||||
async function main() {
|
||||
const options = new Set(process.argv.slice(2));
|
||||
if (options.size === 0 || options.has('--help')) return printHelp();
|
||||
|
||||
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 unknown = [...options].find((option) => (
|
||||
option !== '--generate-key' && option !== '--test-network'
|
||||
));
|
||||
if (unknown) throw new Error(`Unknown option: ${unknown}`);
|
||||
|
||||
const response = await withTimeout(network.command('sping'), 10000, 'Network sping command timed out');
|
||||
console.log(`sping response: ${JSON.stringify(response)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (options.has('--generate-key') || options.has('--test-network')) {
|
||||
await unicryptoReady;
|
||||
const privateKey = await PrivateKey.generate({ strength: 2048 });
|
||||
console.log(`Universa short address: ${privateKey.publicKey.shortAddress58}`);
|
||||
|
||||
if (options.has('--test-network')) await testNetwork(privateKey);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await program.parseAsync();
|
||||
await main();
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
process.exit();
|
||||
}
|
||||
Reference in new issue
Block a user