Simplify sample with universa-core2 API
This commit is contained in:
parent
d84d085d8c
commit
e24914e76a
13
README.md
13
README.md
@ -1,6 +1,6 @@
|
|||||||
# u-js-sample
|
# u-js-sample
|
||||||
|
|
||||||
A small Node.js console script project.
|
A small Node.js command-line example using `universa-core2@2.0.0-alpha.1`.
|
||||||
|
|
||||||
## Initialize and test
|
## Initialize and test
|
||||||
|
|
||||||
@ -9,11 +9,14 @@ npm ci
|
|||||||
scripts/test-network.sh
|
scripts/test-network.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The script uses the topology in `src/universa.json` to verify that the Universa
|
The test loads `src/universa.json` through `Topology`, connects with `Network`,
|
||||||
network is reachable and functioning correctly.
|
authenticates to every available node, and verifies `sping=spong`. The alpha
|
||||||
|
package intentionally has no bundled default topology, so applications must
|
||||||
|
provide one explicitly.
|
||||||
|
|
||||||
## Check
|
## Examples
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run check
|
npm start -- --generate-key
|
||||||
|
npm start -- --test-network
|
||||||
```
|
```
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@ -8,7 +8,6 @@
|
|||||||
"name": "u-js-sample",
|
"name": "u-js-sample",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "^14.0.2",
|
|
||||||
"universa-core2": "2.0.0-alpha.1"
|
"universa-core2": "2.0.0-alpha.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -80,15 +79,6 @@
|
|||||||
"ieee754": "^1.1.13"
|
"ieee754": "^1.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
|
||||||
"version": "14.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
|
|
||||||
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/diffie-hellman": {
|
"node_modules/diffie-hellman": {
|
||||||
"version": "5.0.3",
|
"version": "5.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "u-js-sample",
|
"name": "u-js-sample",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "A Node.js console script sample.",
|
"description": "A small universa-core2 Node.js sample.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -10,13 +10,13 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/index.js",
|
"start": "node src/index.js",
|
||||||
"test:network": "bash scripts/test-network.sh",
|
"test:network": "bash scripts/test-network.sh",
|
||||||
"check": "node --check src/index.js"
|
"check": "node --check src/index.js",
|
||||||
|
"test": "npm run check && npm run test:network"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "^14.0.2",
|
|
||||||
"universa-core2": "2.0.0-alpha.1"
|
"universa-core2": "2.0.0-alpha.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,194 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { readFileSync } from 'node:fs';
|
|
||||||
import { createRequire } from 'node:module';
|
|
||||||
import { dirname, resolve } from 'node:path';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
const {
|
|
||||||
Boss,
|
|
||||||
PrivateKey,
|
|
||||||
PublicKey,
|
|
||||||
SymmetricKey,
|
|
||||||
decode64,
|
|
||||||
encode64,
|
|
||||||
randomBytes,
|
|
||||||
unicryptoReady
|
|
||||||
} = require('universa-core2');
|
|
||||||
|
|
||||||
const projectDirectory = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
||||||
const topologyFile = resolve(projectDirectory, 'src/universa.json');
|
|
||||||
const requestTimeoutMs = 5000;
|
|
||||||
const clientVersion = 3;
|
|
||||||
|
|
||||||
function describeError(error) {
|
|
||||||
if (error instanceof Error) return error.message;
|
|
||||||
try {
|
|
||||||
return JSON.stringify(error);
|
|
||||||
} catch {
|
|
||||||
return String(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTopology() {
|
|
||||||
const topology = JSON.parse(readFileSync(topologyFile, 'utf8'));
|
|
||||||
const nodes = Array.isArray(topology) ? topology : topology.list;
|
|
||||||
|
|
||||||
if (!Array.isArray(nodes) || nodes.length === 0) {
|
|
||||||
throw new Error(`${topologyFile} must contain a non-empty node list`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const node of nodes) {
|
|
||||||
if (!node.number || !node.key || !node.direct_urls?.length) {
|
|
||||||
throw new Error(`Invalid node entry in ${topologyFile}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function request(fetchImpl, method, url, params) {
|
|
||||||
const options = {
|
|
||||||
method,
|
|
||||||
signal: AbortSignal.timeout(requestTimeoutMs)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (method === 'POST') {
|
|
||||||
options.headers = { 'Content-Type': 'application/json' };
|
|
||||||
options.body = JSON.stringify({ requestData64: encode64(Boss.dump(params)) });
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetchImpl(url, options);
|
|
||||||
if (!response.ok) throw new Error(`${method} ${url} returned HTTP ${response.status}`);
|
|
||||||
|
|
||||||
const answer = Boss.load(new Uint8Array(await response.arrayBuffer()));
|
|
||||||
if (answer?.result !== 'ok') throw answer || new Error(`Invalid response from ${url}`);
|
|
||||||
return answer.response;
|
|
||||||
}
|
|
||||||
|
|
||||||
function packedNodeId(node) {
|
|
||||||
const key = typeof node.key === 'string' ? node.key : encode64(node.key);
|
|
||||||
return `${node.number}:${key}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function verifySignedTopology(fetchImpl, node, publicKey, expectedNodeIds) {
|
|
||||||
const response = await request(fetchImpl, 'GET', `${node.direct_urls[0]}/topology`);
|
|
||||||
const packed = response?.packed_data;
|
|
||||||
const signature = response?.signature;
|
|
||||||
|
|
||||||
if (!packed || !signature || !await publicKey.verifyExtended(signature, packed)) {
|
|
||||||
throw new Error('invalid topology signature');
|
|
||||||
}
|
|
||||||
|
|
||||||
const advertised = Boss.load(packed)?.nodes;
|
|
||||||
if (!Array.isArray(advertised)) throw new Error('signed topology has no node list');
|
|
||||||
|
|
||||||
const advertisedNodeIds = new Set(advertised.map(packedNodeId));
|
|
||||||
for (const expectedNodeId of expectedNodeIds) {
|
|
||||||
if (!advertisedNodeIds.has(expectedNodeId)) {
|
|
||||||
throw new Error('signed topology does not contain every configured node');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openSession(fetchImpl, node, publicKey, privateKey) {
|
|
||||||
const baseUrl = node.direct_urls[0];
|
|
||||||
const signatureOptions = { pssHash: 'sha512' };
|
|
||||||
const clientNonce = randomBytes(47);
|
|
||||||
const clientKey = await privateKey.publicKey.pack();
|
|
||||||
const connection = await request(fetchImpl, 'POST', `${baseUrl}/connect`, {
|
|
||||||
client_key: clientKey,
|
|
||||||
client_version: clientVersion
|
|
||||||
});
|
|
||||||
const serverVersion = connection.server_version || 1;
|
|
||||||
const version = Math.min(serverVersion, clientVersion);
|
|
||||||
const sessionId = connection.session_id;
|
|
||||||
const authenticationData = Boss.dump({
|
|
||||||
client_nonce: clientNonce,
|
|
||||||
server_nonce: connection.server_nonce,
|
|
||||||
client_version: clientVersion,
|
|
||||||
server_version: serverVersion
|
|
||||||
});
|
|
||||||
const token = await request(fetchImpl, 'POST', `${baseUrl}/get_token`, {
|
|
||||||
data: authenticationData,
|
|
||||||
signature: await privateKey.sign(authenticationData, signatureOptions),
|
|
||||||
session_id: sessionId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!await publicKey.verify(token.data, token.signature, signatureOptions)) {
|
|
||||||
throw new Error('invalid authentication signature');
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenData = Boss.load(token.data);
|
|
||||||
if (encode64(tokenData.client_nonce) !== encode64(clientNonce)) {
|
|
||||||
throw new Error('authentication nonce mismatch');
|
|
||||||
}
|
|
||||||
|
|
||||||
const decryptedToken = Boss.load(await privateKey.decrypt(tokenData.encrypted_token));
|
|
||||||
const sessionKey = new SymmetricKey({ keyBytes: decryptedToken.sk });
|
|
||||||
|
|
||||||
return async (name, params = {}) => {
|
|
||||||
const data = Boss.dump({ command: name, params });
|
|
||||||
const encrypted = version >= 2
|
|
||||||
? await sessionKey.etaEncrypt(data)
|
|
||||||
: await sessionKey.encrypt(data);
|
|
||||||
const response = await request(fetchImpl, 'POST', `${baseUrl}/command`, {
|
|
||||||
command: 'command',
|
|
||||||
params: encrypted,
|
|
||||||
session_id: sessionId
|
|
||||||
});
|
|
||||||
const decrypted = version >= 2
|
|
||||||
? await sessionKey.etaDecrypt(response.result)
|
|
||||||
: await sessionKey.decrypt(response.result);
|
|
||||||
const result = Boss.load(decrypted);
|
|
||||||
if (result.error) throw result.error;
|
|
||||||
return result.result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testNode(fetchImpl, node, privateKey, expectedNodeIds) {
|
|
||||||
const keyBytes = typeof node.key === 'string' ? decode64(node.key) : node.key;
|
|
||||||
const publicKey = await PublicKey.unpack(keyBytes);
|
|
||||||
await verifySignedTopology(fetchImpl, node, publicKey, expectedNodeIds);
|
|
||||||
const command = await openSession(fetchImpl, node, publicKey, privateKey);
|
|
||||||
const hello = await command('hello');
|
|
||||||
if (hello?.status !== 'OK') throw new Error(`hello returned ${hello?.status || 'no status'}`);
|
|
||||||
const ping = await command('sping');
|
|
||||||
if (ping?.sping !== 'spong') throw new Error('sping did not return spong');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main(fetchImpl) {
|
|
||||||
const nodes = loadTopology();
|
|
||||||
const expectedNodeIds = new Set(nodes.map(packedNodeId));
|
|
||||||
const quorum = Math.max(1, Math.ceil(nodes.length * 0.4));
|
|
||||||
const privateKey = await PrivateKey.generate({ strength: 2048 });
|
|
||||||
|
|
||||||
console.log(`Testing ${nodes.length} Universa nodes from ${topologyFile}`);
|
|
||||||
const results = await Promise.all(nodes.map(async (node) => {
|
|
||||||
try {
|
|
||||||
await testNode(fetchImpl, node, privateKey, expectedNodeIds);
|
|
||||||
console.log(`PASS ${node.name}: signed topology, authenticated hello, sping=spong`);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`FAIL ${node.name}: ${describeError(error)}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const passed = results.filter(Boolean).length;
|
|
||||||
if (passed < quorum) {
|
|
||||||
throw new Error(`Universa network quorum failed: ${passed}/${nodes.length} nodes passed; ${quorum} required`);
|
|
||||||
}
|
|
||||||
console.log(`Universa network is functioning: ${passed}/${nodes.length} nodes passed (${quorum} required)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await unicryptoReady;
|
|
||||||
const fetchImpl = globalThis.fetch;
|
|
||||||
if (!fetchImpl) throw new Error('This test requires the Node.js fetch API');
|
|
||||||
await main(fetchImpl);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(describeError(error));
|
|
||||||
process.exitCode = 1;
|
|
||||||
}
|
|
||||||
@ -2,4 +2,4 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||||
exec node scripts/test-network.js
|
exec node src/index.js --test-network
|
||||||
|
|||||||
228
src/index.js
228
src/index.js
@ -1,199 +1,103 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { createRequire } from 'node:module';
|
import { dirname, resolve } from 'node:path';
|
||||||
import { createConnection } from 'node:net';
|
|
||||||
import { basename, dirname, resolve } from 'node:path';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { Command } from 'commander';
|
import {
|
||||||
|
Network,
|
||||||
|
PrivateKey,
|
||||||
|
Topology,
|
||||||
|
unicryptoReady
|
||||||
|
} from 'universa-core2';
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const sourceDirectory = dirname(fileURLToPath(import.meta.url));
|
||||||
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
const topologyFile = resolve(sourceDirectory, 'universa.json');
|
||||||
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) {
|
async function withTimeout(promise, milliseconds, message) {
|
||||||
let timeoutId;
|
let timeoutId;
|
||||||
const timeout = new Promise((_, reject) => {
|
const timeout = new Promise((_, reject) => {
|
||||||
timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
|
timeoutId = setTimeout(() => reject(new Error(message)), milliseconds);
|
||||||
});
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
return require('universa-core');
|
return await Promise.race([promise, timeout]);
|
||||||
} finally {
|
} finally {
|
||||||
if (windowDescriptor) Object.defineProperty(globalThis, 'window', windowDescriptor);
|
clearTimeout(timeoutId);
|
||||||
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() {
|
async function loadTopology() {
|
||||||
return withUniversaWasmLoader(async () => {
|
const packed = JSON.parse(await readFile(topologyFile, 'utf8'));
|
||||||
const { PrivateKey } = require('unicrypto');
|
return Topology.load(packed);
|
||||||
return PrivateKey.generate({ strength: 2048 });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTopology(Topology) {
|
async function testNetwork(privateKey) {
|
||||||
return withUniversaWasmLoader(async () => {
|
const topology = await loadTopology();
|
||||||
const packedTopology = readTopology();
|
const network = new Network(privateKey, {
|
||||||
return Topology.load(packedTopology);
|
topology,
|
||||||
});
|
directConnection: true
|
||||||
}
|
|
||||||
|
|
||||||
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 = '') {
|
console.log(`Testing ${topology.size()} Universa nodes from ${topologyFile}`);
|
||||||
socket.destroy();
|
await withTimeout(network.connect(), 15_000, 'Network connection timed out');
|
||||||
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;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
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`);
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.once('connect', () => finish(true));
|
console.log(`Universa network is functioning: ${passed}/${nodes.length} nodes passed`);
|
||||||
socket.once('timeout', () => finish(false, 'TCP connection timed out'));
|
|
||||||
socket.once('error', (error) => finish(false, error.message));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assertReachableTopology(directConnection) {
|
function printHelp() {
|
||||||
const topology = readTopology();
|
console.log(`Usage: u-js-sample [options]
|
||||||
const urls = topology.list.flatMap((node) => (
|
|
||||||
directConnection ? node.direct_urls : node.domain_urls
|
|
||||||
) || []);
|
|
||||||
|
|
||||||
if (urls.length === 0) {
|
Options:
|
||||||
throw new Error(`Topology file does not contain ${directConnection ? 'direct_urls' : 'domain_urls'} entries: ${topologyFile}`);
|
--generate-key Generate a private key and print its short address
|
||||||
|
--test-network Test every node in src/universa.json
|
||||||
|
--help Show this help`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(urls.map(probeEndpoint));
|
async function main() {
|
||||||
if (results.some((result) => result.ok)) return;
|
const options = new Set(process.argv.slice(2));
|
||||||
|
if (options.size === 0 || options.has('--help')) return printHelp();
|
||||||
|
|
||||||
const details = results
|
const unknown = [...options].find((option) => (
|
||||||
.map(({ url, error }) => ` ${url}/topology: ${error}`)
|
option !== '--generate-key' && option !== '--test-network'
|
||||||
.join('\n');
|
));
|
||||||
|
if (unknown) throw new Error(`Unknown option: ${unknown}`);
|
||||||
|
|
||||||
throw new Error(`No reachable topology endpoints in ${topologyFile}:\n${details}`);
|
if (options.has('--generate-key') || options.has('--test-network')) {
|
||||||
}
|
await unicryptoReady;
|
||||||
|
const privateKey = await PrivateKey.generate({ strength: 2048 });
|
||||||
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}`);
|
console.log(`Universa short address: ${privateKey.publicKey.shortAddress58}`);
|
||||||
|
|
||||||
if (options.testNetwork) {
|
if (options.has('--test-network')) await testNetwork(privateKey);
|
||||||
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 {
|
try {
|
||||||
await program.parseAsync();
|
await main();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error instanceof Error ? error.message : error);
|
console.error(error instanceof Error ? error.message : error);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
} finally {
|
|
||||||
process.exit();
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user