started on server api

This commit is contained in:
2025-07-29 17:10:46 +03:00
parent f8c159909e
commit 5e6326c516
13 changed files with 1079 additions and 168 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,8 @@
"license": "MIT",
"main": "build/src/index.js",
"scripts": {
"build": "tsc && electron-rebuild",
"build": "tsc && npm run copy-wasm && electron-rebuild",
"copy-wasm": "mkdir -p ./build/src/lite-node/wasm && cp ./src/lite-node/wasm/memory-pow.wasm.full ./build/src/lite-node/wasm/",
"electron:start-live": "node ./live-runner.js",
"electron:start": "npm run build && electron --inspect=5858 ./",
"electron:pack": "npm run build && electron-builder build --dir -c ./electron-builder.config.json",
@@ -45,11 +46,13 @@
"adm-zip": "^0.5.16",
"bs58": "^6.0.0",
"chokidar": "^3.6.0",
"decimal.js": "^10.6.0",
"electron-is-dev": "~2.0.0",
"electron-serve": "~1.1.0",
"electron-unhandled": "~4.0.1",
"electron-updater": "^5.3.0",
"electron-window-state": "^5.0.3",
"express": "^5.1.0",
"tweetnacl": "^1.0.3"
},
"devDependencies": {
@@ -57,6 +60,7 @@
"@types/node": "^24.1.0",
"electron": "^32.3.1",
"electron-builder": "^25.1.8",
"electron-rebuild": "^3.2.9",
"shelljs": "^0.8.5",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"

View File

@@ -48,6 +48,9 @@ export class LiteNodeClient {
private knownPeers: Set<string> = new Set();
private remoteAddress?: string;
public lastKnownBlockHeight: number | null = null;
public lastKnownBlockTimestamp: number | null = null;
constructor(
private host: string,
private port: number = 12392,
@@ -126,7 +129,96 @@ export class LiteNodeClient {
}
}
console.log(`✅ Total known peers: ${discoveredPeers.size}`);
// console.log(`✅ Total known peers: ${discoveredPeers.size}`);
}
private handleBlockSummaries(payload: Buffer) {
const BLOCK_SIGNATURE_LENGTH = 128;
const PUBLIC_KEY_LENGTH = 32;
const INT_LENGTH = 4;
const LONG_LENGTH = 8;
const BLOCK_SUMMARY_V2_LENGTH =
BLOCK_SIGNATURE_LENGTH + // signature
PUBLIC_KEY_LENGTH + // minter public key
INT_LENGTH + // online accounts count
LONG_LENGTH + // timestamp
INT_LENGTH + // transaction count
BLOCK_SIGNATURE_LENGTH; // reference
const blockSummaries: {
height: number;
signature: Buffer;
minterPublicKey: Buffer;
onlineAccountsCount: number;
timestamp: number;
transactionsCount: number;
reference: Buffer;
}[] = [];
if (payload.length === 0) return blockSummaries;
let offset = 0;
const heightStart = payload.readInt32BE(offset);
offset += 4;
let currentHeight = heightStart;
while (offset + BLOCK_SUMMARY_V2_LENGTH <= payload.length) {
const signature = payload.subarray(
offset,
offset + BLOCK_SIGNATURE_LENGTH
);
offset += BLOCK_SIGNATURE_LENGTH;
const minterPublicKey = payload.subarray(
offset,
offset + PUBLIC_KEY_LENGTH
);
offset += PUBLIC_KEY_LENGTH;
const onlineAccountsCount = payload.readInt32BE(offset);
offset += INT_LENGTH;
const timestamp = payload.readBigInt64BE(offset);
offset += LONG_LENGTH;
const transactionsCount = payload.readInt32BE(offset);
offset += INT_LENGTH;
const reference = payload.subarray(
offset,
offset + BLOCK_SIGNATURE_LENGTH
);
offset += BLOCK_SIGNATURE_LENGTH;
blockSummaries.push({
height: currentHeight,
signature,
minterPublicKey,
onlineAccountsCount,
timestamp: Number(timestamp),
transactionsCount,
reference,
});
currentHeight++;
}
if (blockSummaries.length > 0) {
const latestSummary = blockSummaries[blockSummaries.length - 1];
this.lastKnownBlockHeight = latestSummary.height;
this.lastKnownBlockTimestamp = latestSummary.timestamp;
// Optionally: update peer stats in the manager
this.manager.updatePeerChainTip?.(
`${this.host}:${this.port}`,
latestSummary.height,
latestSummary.timestamp
);
}
// TODO: Store or evaluate summaries to determine sync quality
}
private isValidIp(ip: string): boolean {
@@ -261,8 +353,16 @@ export class LiteNodeClient {
case MessageType.PEERS_V2:
this.handlePeerV2(payload);
break;
default:
// console.warn(`⚠️ Unhandled message type: ${messageType}`);
case MessageType.BLOCK_SUMMARIES_V2:
this.handleBlockSummaries(payload);
break;
default: {
if (messageType?.toString().includes('7')) {
console.warn(
`⚠️ Unhandled message type: ${messageType}, ${this.host}`
);
}
}
}
}
});
@@ -320,7 +420,7 @@ export class LiteNodeClient {
}
}
startPinging(intervalMs: number = 30000) {
startPinging(intervalMs: number = 20000) {
if (this.pingInterval) clearInterval(this.pingInterval);
this.pingInterval = setInterval(() => {
if (!this.socket || this.socket.destroyed) return;

View File

@@ -8,6 +8,12 @@ type PeerStats = {
lastFailure?: number;
};
function safeBigIntToNumber(big: bigint): number {
const max = BigInt(Number.MAX_SAFE_INTEGER);
if (big > max) throw new Error(`Timestamp too large: ${big.toString()}`);
return Number(big);
}
export class PeerManager {
private peerStatsMap = new Map<string, PeerStats>();
@@ -15,13 +21,20 @@ export class PeerManager {
public connectedClients = new Map<string, LiteNodeClient>();
private seedPeers: string[];
private readonly MAX_BLOCK_LAG = 2; // block height difference tolerance
private readonly MAX_TIME_LAG = 2 * 60 * 1000; // 10 minutes in ms
private peerChainTips: Map<string, { height: number; timestamp: number }> =
new Map();
constructor(seedPeers: string[], maxConnections = 10) {
this.seedPeers = seedPeers;
this.maxConnections = maxConnections;
}
async initialize() {
await this.tryConnectToPeers(this.seedPeers);
console.log('initialized');
this.tryConnectToPeers(this.seedPeers);
this.startPruneLoop();
// Start peer discovery loop
this.discoveryLoop();
@@ -44,6 +57,14 @@ export class PeerManager {
this.peerStatsMap.set(peerKey, stats);
}
public updatePeerChainTip(
peerKey: string,
height: number,
timestamp: number
) {
this.peerChainTips.set(peerKey, { height, timestamp });
}
private async tryConnectToPeers(peers: string[]) {
console.log(
`[${new Date().toLocaleTimeString()}] 🔌 Total list peers: ${this.getConnectedCount()}`
@@ -83,6 +104,7 @@ export class PeerManager {
}
private async discoveryLoop() {
console.log('hello');
setInterval(async () => {
console.log(`🔌 Total connected peers: ${this.getConnectedCount()}`);
if (this.connectedClients.size >= this.maxConnections) return;
@@ -92,6 +114,59 @@ export class PeerManager {
}, 10_000); // Try every 10 seconds
}
public pruneStalePeers(latestHeight: number, latestTimestamp: number) {
for (const [peerKey, client] of this.connectedClients.entries()) {
if (
client.lastKnownBlockHeight === null ||
client.lastKnownBlockTimestamp === null
) {
continue; // skip peers we haven't heard from
}
const heightLag = latestHeight - client.lastKnownBlockHeight;
const timeLag = latestTimestamp - client.lastKnownBlockTimestamp;
if (heightLag > this.MAX_BLOCK_LAG || timeLag > this.MAX_TIME_LAG) {
console.warn(
`❌ Pruning stale peer ${peerKey} (lagging by ${heightLag} blocks, ${timeLag / 1000}s)`
);
this.removePeer(peerKey);
}
}
}
private startPruneLoop() {
setInterval(() => {
let maxHeight = 0;
let maxTimestamp = 0;
for (const client of this.connectedClients.values()) {
if (
client.lastKnownBlockHeight &&
client.lastKnownBlockHeight > maxHeight
) {
maxHeight = client.lastKnownBlockHeight;
}
if (
client.lastKnownBlockTimestamp &&
client.lastKnownBlockTimestamp > maxTimestamp
) {
maxTimestamp = client.lastKnownBlockTimestamp;
}
}
if (maxHeight && maxTimestamp) {
console.log(
`🧹 Pruning check: maxHeight=${maxHeight}, maxTimestamp=${new Date(
maxTimestamp
).toLocaleTimeString()}`
);
this.pruneStalePeers(maxHeight, maxTimestamp);
}
}, 30_000); // Run every 30 seconds
}
getConnectedCount() {
return this.connectedClients.size;
}
@@ -100,11 +175,22 @@ export class PeerManager {
return Array.from(this.connectedClients.values());
}
getRandomClient(): LiteNodeClient | null {
const clients = Array.from(this.connectedClients.values());
if (clients.length === 0) return null;
const randomIndex = Math.floor(Math.random() * clients.length);
return clients[randomIndex];
public getBestClient(): LiteNodeClient | null {
const sorted = [...this.connectedClients.values()]
.filter(
(c) =>
c.lastKnownBlockHeight !== null && c.lastKnownBlockTimestamp !== null
)
.sort((a, b) => {
const heightDiff =
(b.lastKnownBlockHeight ?? 0) - (a.lastKnownBlockHeight ?? 0);
if (heightDiff !== 0) return heightDiff;
return (
(b.lastKnownBlockTimestamp ?? 0) - (a.lastKnownBlockTimestamp ?? 0)
);
});
return sorted[0] || null;
}
removePeer(peerKey: string) {

View File

@@ -1,9 +1,12 @@
// accountApi.ts
import { handleAccountBalance } from '../messages/handlers';
import { handleAccount, handleAccountBalance } from '../messages/handlers';
import { getRandomClient, startPeerManager } from '../peerService';
import { MessageType } from '../protocol/messageTypes';
import { createGetAccountBalancePayload } from '../protocol/payloads';
import {
createGetAccountBalancePayload,
createGetAccountMessagePayload,
} from '../protocol/payloads';
export async function getAccountBalance(address: string): Promise<any> {
const client = getRandomClient();
@@ -20,3 +23,19 @@ export async function getAccountBalance(address: string): Promise<any> {
(async () => {
await startPeerManager();
})();
export async function getAccount(address: string): Promise<any> {
const client = getRandomClient();
if (!client) throw new Error('No available peers');
const res: Buffer = await client.sendRequest(
MessageType.GET_ACCOUNT,
createGetAccountMessagePayload(address)
);
return handleAccount(res);
}
(async () => {
await startPeerManager();
})();

View File

@@ -0,0 +1,39 @@
import express from 'express';
import { getAccount, getAccountBalance } from './account';
export async function createHttpServer() {
const app = express();
app.use(express.json());
app.get('/addresses/balance/:address', async (req, res) => {
const address = req.params.address;
try {
const balance = await getAccountBalance(address); // should return string like "969.59515719"
res.type('text').send(+balance); // ✅ sends plain text response
} catch (err: any) {
res.status(500).type('text').send(`Error: ${err.message}`);
}
});
app.get('/addresses/:address', async (req, res) => {
const address = req.params.address;
try {
const accountInfo = await getAccount(address); // should return string like "969.59515719"
res.json(accountInfo);
} catch (err: any) {
res.status(500).type('text').send(`Error: ${err.message}`);
}
});
app.get('/admin/apikey/test', async (req, res) => {
try {
res.type('text').send(true); // ✅ sends plain text response
} catch (err: any) {
res.status(500).type('text').send(`Error: ${err.message}`);
}
});
const port = 12395;
app.listen(port, () => {
console.log(`🚀 HTTP API server running at http://localhost:${port}`);
});
}

View File

@@ -1,23 +0,0 @@
// src/lite-node/clientInstance.ts
import { LiteNodeClient } from './LiteNodeClient';
const SEED_PEERS = ['127.0.0.1'];
let client: LiteNodeClient | null = null;
export async function getClient(): Promise<LiteNodeClient> {
if (client) return client;
for (const ip of SEED_PEERS) {
const instance = new LiteNodeClient(ip);
try {
await instance.connect();
client = instance;
return client;
} catch (err) {
console.warn(`❌ Failed to connect to ${ip}:`, err);
}
}
throw new Error('No seed peers could be connected');
}

View File

@@ -1,71 +0,0 @@
// src/main.ts
import readline from 'readline';
import { MessageType } from './protocol/messageTypes';
import { LiteNodeClient } from './LiteNodeClient';
import { createGetAccountBalancePayload } from './protocol/payloads';
const SEED_PEERS = ['127.0.0.1'];
let activeClient: LiteNodeClient | null = null;
async function main() {
process.once('SIGINT', () => {
console.log('\n🛑 Caught SIGINT, closing client...');
activeClient?.close();
process.exit(0);
});
for (const ip of SEED_PEERS) {
const client = new LiteNodeClient(ip);
try {
await client.connect();
activeClient = client;
console.log(`✅ Connected to ${ip}`);
break;
} catch (err) {
console.warn(`❌ Failed to connect to ${ip}:`, err);
}
}
if (!activeClient) {
console.error('❌ Could not connect to any peer');
process.exit(1);
}
// ⌨️ Start command line input
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.on('line', (input) => {
const trimmed = input.trim();
if (trimmed.startsWith('balance')) {
const parts = trimmed.split(' ');
const address = parts[1];
if (!address) return console.log('⚠️ Usage: balance <QortalAddress>');
const payload = createGetAccountBalancePayload(address, 0);
activeClient!.sendMessage(MessageType.GET_ACCOUNT_BALANCE, payload);
console.log(`📤 Sent GET_ACCOUNT_BALANCE for ${address}`);
}
// More commands can go here...
else if (trimmed === 'exit') {
rl.close();
} else {
console.log('❓ Unknown command');
}
});
rl.on('close', () => {
console.log('👋 Exiting...');
activeClient?.close();
process.exit(0);
});
console.log('🟢 Enter a command (e.g., `balance Q...`, `exit`)');
}
main();

View File

@@ -1,38 +0,0 @@
import { handleAccountBalance } from './messages/handlers';
import { PeerManager } from './PeerManager';
import { MessageType } from './protocol/messageTypes';
import { createGetAccountBalancePayload } from './protocol/payloads';
const SEED_PEERS = ['127.0.0.1'];
async function main() {
console.log('🚀 Starting PeerManager...');
const manager = new PeerManager(SEED_PEERS);
await manager.initialize();
console.log(`✅ Connected to ${manager.getConnectedCount()} peers.`);
await new Promise((res) =>
setTimeout(() => {
res(null);
}, 10000)
);
const client = manager.getRandomClient();
if (client) {
// client.sendMessage(MessageType.PING, createPingPayload());
const account = 'QP9Jj4S3jpCgvPnaABMx8VWzND3qpji6rP';
const res: Buffer = await client.sendRequest(
MessageType.GET_ACCOUNT_BALANCE,
createGetAccountBalancePayload(account, 0)
);
handleAccountBalance(res);
console.log('📡 Sent PING message to random peer');
} else {
console.warn('⚠️ No connected clients to send message');
}
// You can now use manager.getConnectedClients() to interact with them
}
main();

View File

@@ -1,5 +1,11 @@
import bs58 from 'bs58';
import Decimal from 'decimal.js';
function toBigDecimal(amountBigInt) {
return new Decimal(amountBigInt.toString()).div(1e8);
}
export async function handleAccountBalance(payload: Buffer) {
console.log('payload100', payload);
if (payload.length < 41) {
@@ -18,6 +24,8 @@ export async function handleAccountBalance(payload: Buffer) {
console.log('🪙 Asset ID:', assetId.toString());
console.log('💰 Balance:', balance.toString());
return toBigDecimal(balance);
// Optionally store or use the data here
}
@@ -75,5 +83,17 @@ export async function handleAccount(payload: Buffer) {
console.log('📈 Adjustment:', blocksMintedAdjustment);
console.log('📉 Penalty:', blocksMintedPenalty);
return {
address: address,
reference: bs58.encode(reference),
publicKey: bs58.encode(publicKey),
defaultGroupId: defaultGroupId,
flags: flags,
level: level,
blocksMinted: blocksMinted,
blocksMintedAdjustment: blocksMintedAdjustment,
blocksMintedPenalty: blocksMintedPenalty,
};
// Use/store this information as needed
}

View File

@@ -1,7 +1,40 @@
// peerService.ts
import { PeerManager } from './PeerManager';
const SEED_PEERS = ['127.0.0.1'];
const SEED_PEERS = [
'node1.qortal.org',
'node2.qortal.org',
'node3.qortal.org',
'node4.qortal.org',
'node5.qortal.org',
'node6.qortal.org',
'node7.qortal.org',
'node8.qortal.org',
'node9.qortal.org',
'node10.qortal.org',
'node11.qortal.org',
'node12.qortal.org',
'node13.qortal.org',
'node14.qortal.org',
'node15.qortal.org',
'node.qortal.ru',
'node2.qortal.ru',
'node3.qortal.ru',
'node.qortal.uk',
'qnode1.crowetic.com',
'bootstrap.qortal.org',
'proxynodes.qortal.link',
'api.qortal.org',
'bootstrap2-ssh.qortal.org',
'bootstrap3-ssh.qortal.org',
'node2.qortalnodes.live',
'node3.qortalnodes.live',
'node4.qortalnodes.live',
'node5.qortalnodes.live',
'node6.qortalnodes.live',
'node7.qortalnodes.live',
'node8.qortalnodes.live',
];
const manager = new PeerManager(SEED_PEERS);
@@ -16,7 +49,7 @@ export async function startPeerManager() {
}
export function getRandomClient() {
return manager.getRandomClient();
return manager.getBestClient();
}
export function getPeerManager() {

View File

@@ -5,7 +5,7 @@ export enum MessageType {
PING = 11,
PEERS_V2 = 20,
GET_PEERS = 21,
BLOCK_SUMMARIES_V2 = 72,
ACCOUNT = 160,
GET_ACCOUNT = 161,
ACCOUNT_BALANCE = 170,

View File

@@ -29,6 +29,8 @@ const AdmZip = require('adm-zip');
import { join } from 'path';
import { myCapacitorApp } from '.';
import { LiteNodeClient } from './lite-node';
import { startPeerManager } from './lite-node/peerService';
import { createHttpServer } from './lite-node/api/httpserver';
const fs = require('fs');
const path = require('path');
@@ -174,6 +176,9 @@ export class ElectronCapacitorApp {
preload: preloadPath,
},
});
// await startPeerManager();
// await createHttpServer();
this.mainWindowState.manage(this.MainWindow);
if (this.CapacitorFileConfig.backgroundColor) {
@@ -489,7 +494,3 @@ const liteNode = new LiteNodeClient('your.qortal.peer.ip');
ipcMain.handle('liteNode:connect', async () => {
await liteNode.connect();
});
ipcMain.handle('liteNode:send', async (_event, type: number, payload: any) => {
liteNode.sendMessage(type, payload);
});