jaelis-node 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -169,7 +169,24 @@ Options:
169
169
 
170
170
  ## Block Sync & Node Status
171
171
 
172
- Your node automatically syncs with the JAELIS network:
172
+ Your node automatically syncs with the JAELIS network using **WebSocket push notifications** (like Ethereum's `newHeads` subscription) for real-time updates:
173
+
174
+ ```
175
+ [SYNC] Starting sync from https://rpc.jaelis.io
176
+ [SYNC] Syncing 25 blocks (1 → 25)
177
+ [SYNC] Local height: 25/139
178
+ [SYNC] Syncing 25 blocks (26 → 50)
179
+ ...
180
+ [SYNC] Local height: 139/139
181
+ [SYNC] WebSocket connected! Subscribing to newHeads...
182
+ [SYNC] ✓ Subscribed to newHeads (id: 0x1)
183
+ [SYNC] Listening for new blocks via WebSocket...
184
+ ```
185
+
186
+ **Sync Features:**
187
+ - **Smart batching** - 25 blocks during initial sync, reduces as you get closer
188
+ - **WebSocket subscriptions** - Real-time block notifications (no polling!)
189
+ - **Automatic fallback** - Falls back to HTTP polling if WebSocket unavailable
173
190
 
174
191
  ```bash
175
192
  # Check sync status
@@ -215,6 +232,42 @@ curl -X POST https://rpc.jaelis.io \
215
232
  }
216
233
  ```
217
234
 
235
+ ## WebSocket Subscriptions
236
+
237
+ Connect to the WebSocket endpoint for real-time updates:
238
+
239
+ ```javascript
240
+ const WebSocket = require('ws');
241
+ const ws = new WebSocket('wss://rpc.jaelis.io/ws');
242
+
243
+ ws.on('open', () => {
244
+ // Subscribe to new block headers
245
+ ws.send(JSON.stringify({
246
+ jsonrpc: '2.0',
247
+ method: 'jaelis_subscribe',
248
+ params: ['newHeads'],
249
+ id: 1
250
+ }));
251
+ });
252
+
253
+ ws.on('message', (data) => {
254
+ const msg = JSON.parse(data);
255
+ if (msg.method === 'jaelis_subscription') {
256
+ console.log('New block:', msg.params.result.number);
257
+ }
258
+ });
259
+ ```
260
+
261
+ **Available Subscriptions:**
262
+ | Subscription | Description |
263
+ |--------------|-------------|
264
+ | `newHeads` | New block headers as they're produced (real-time sync) |
265
+ | `newPendingTransactions` | Transaction hashes entering mempool |
266
+ | `logs` | Contract event logs (with filter options) |
267
+ | `syncing` | Sync status changes |
268
+
269
+ > **Note:** JAELIS uses Proof of Efficient Consensus (PoEC), not mining. Blocks are produced by validators, not mined. The `newHeads` subscription keeps your node synced in real-time.
270
+
218
271
  ## RPC Methods
219
272
 
220
273
  Your node exposes standard JSON-RPC plus JAELIS-specific methods:
@@ -36,16 +36,29 @@ const BANNER = `
36
36
  `;
37
37
 
38
38
  // Network configurations
39
+ // Bootstrap nodes use libp2p multiaddr format: /dns4/host/tcp/port/p2p/PEER_ID
40
+ //
41
+ // IMPORTANT: libp2p requires peer ID for direct dialing!
42
+ // Format: /dns4/rpc.jaelis.io/tcp/30304/p2p/12D3KooW...
43
+ //
44
+ // How it works (like Ethereum):
45
+ // 1. Main node generates persistent peer ID on first run
46
+ // 2. Peer ID is saved to disk (jaelis-data/peer-id.json)
47
+ // 3. Main node logs bootstrap multiaddr on startup
48
+ // 4. Update bootstrapNodes here with that multiaddr
49
+ //
50
+ // If no bootstrap nodes configured, RPC fallback sync is used automatically
39
51
  const NETWORKS = {
40
52
  testnet: {
41
53
  chainId: 4545,
42
54
  name: 'JAELIS Testnet',
43
55
  symbol: 'tJAELIS',
44
56
  rpcUrl: 'https://rpc.jaelis.io',
45
- // Bootstrap via DNS - rpc.jaelis.io resolves to 73.116.51.60
46
- // No peer ID needed - learned during libp2p handshake
57
+ // Testnet P2P port: 30304 (mainnet: 30303)
58
+ // Bootstrap nodes will be added once main node generates peer ID
59
+ // Until then, nodes sync via RPC fallback (works perfectly)
47
60
  bootstrapNodes: [
48
- '/dns4/rpc.jaelis.io/tcp/30303'
61
+ // Will be: '/dns4/rpc.jaelis.io/tcp/30304/p2p/PEER_ID'
49
62
  ]
50
63
  },
51
64
  mainnet: {
package/lib/index.js CHANGED
@@ -333,6 +333,12 @@ class NodeWalletConfig {
333
333
  }
334
334
 
335
335
  // JAELIS Network Endpoints - users connect here automatically
336
+ // Bootstrap nodes use libp2p multiaddr format: /dns4/host/tcp/port/p2p/PEER_ID
337
+ // External nodes connect via DNS which resolves to the main node's public IP
338
+ //
339
+ // IMPORTANT: Peer ID is required for libp2p dialing!
340
+ // The peer ID is generated on first run and persisted to disk.
341
+ // Main node logs bootstrap info on startup - update this when it changes.
336
342
  const JAELIS_NETWORKS = {
337
343
  testnet: {
338
344
  name: 'JAELIS Testnet',
@@ -341,8 +347,12 @@ const JAELIS_NETWORKS = {
341
347
  rpcUrl: 'https://rpc.jaelis.io',
342
348
  wsUrl: 'wss://rpc.jaelis.io/ws',
343
349
  explorerUrl: 'https://explorer.jaelis.io',
350
+ // Testnet uses port 30304 (mainnet uses 30303)
351
+ // Peer ID will be added after main node first run
352
+ // For now, P2P will fallback to RPC sync until peer ID is configured
344
353
  bootstrapNodes: [
345
- '/dns4/rpc.jaelis.io/tcp/30303'
354
+ // Format: /dns4/rpc.jaelis.io/tcp/30304/p2p/PEER_ID
355
+ // The main node outputs this on startup - update here when available
346
356
  ]
347
357
  },
348
358
  mainnet: {
@@ -1204,8 +1214,9 @@ class EmbeddedBlockchain {
1204
1214
  }
1205
1215
  }
1206
1216
 
1207
- class EmbeddedNetwork {
1217
+ class EmbeddedNetwork extends EventEmitter {
1208
1218
  constructor(options = {}) {
1219
+ super();
1209
1220
  this.port = options.port || 30303;
1210
1221
  this.host = options.host || '0.0.0.0';
1211
1222
  this.maxPeers = options.maxPeers || 50;
@@ -1221,16 +1232,24 @@ class EmbeddedNetwork {
1221
1232
  }
1222
1233
 
1223
1234
  async start() {
1224
- // Try libp2p first, fallback to RPC sync
1225
- try {
1226
- await this._startLibp2p();
1227
- this.isP2PConnected = true;
1228
- console.log(`[NETWORK] libp2p P2P network started on port ${this.port}`);
1229
- } catch (err) {
1230
- console.log(`[NETWORK] P2P failed (${err.message}), using RPC fallback`);
1231
- await this._startRPCFallback();
1232
- this.isRPCFallback = true;
1233
- console.log(`[NETWORK] RPC fallback active - syncing from ${this.remoteRpc}`);
1235
+ // ALWAYS start RPC sync first - this is reliable through Cloudflare
1236
+ // P2P is optional/additional for peer discovery
1237
+ console.log(`[NETWORK] Starting RPC sync from ${this.remoteRpc}`);
1238
+ await this._startRPCFallback();
1239
+ this.isRPCFallback = true;
1240
+
1241
+ // Try P2P alongside (optional - for peer discovery when bootstrap nodes available)
1242
+ if (this.bootstrapNodes && this.bootstrapNodes.length > 0) {
1243
+ try {
1244
+ await this._startLibp2p();
1245
+ this.isP2PConnected = true;
1246
+ console.log(`[NETWORK] P2P network also started on port ${this.port}`);
1247
+ } catch (err) {
1248
+ console.log(`[NETWORK] P2P unavailable (${err.message}), continuing with RPC only`);
1249
+ }
1250
+ } else {
1251
+ console.log(`[NETWORK] No bootstrap nodes configured - using RPC sync only`);
1252
+ console.log(`[NETWORK] This is fine! RPC sync is reliable and works through firewalls.`);
1234
1253
  }
1235
1254
  }
1236
1255
 
@@ -1296,7 +1315,8 @@ class EmbeddedNetwork {
1296
1315
  remoteHeight: 0,
1297
1316
  syncing: false,
1298
1317
  lastSync: null,
1299
- blocksDownloaded: 0
1318
+ blocksDownloaded: 0,
1319
+ mode: 'polling' // 'websocket' or 'polling'
1300
1320
  };
1301
1321
 
1302
1322
  // Helper to make RPC calls to remote node
@@ -1322,7 +1342,7 @@ class EmbeddedNetwork {
1322
1342
  headers: {
1323
1343
  'Content-Type': 'application/json',
1324
1344
  'Content-Length': Buffer.byteLength(data),
1325
- 'User-Agent': 'JAELIS-Node/1.7.0' // Node sync traffic - exempt from rate limits
1345
+ 'User-Agent': 'JAELIS-Node/1.8.0' // Node sync traffic
1326
1346
  }
1327
1347
  }, (res) => {
1328
1348
  let body = '';
@@ -1342,44 +1362,52 @@ class EmbeddedNetwork {
1342
1362
  });
1343
1363
  };
1344
1364
 
1345
- // Sync blocks from remote RPC using JAELIS NATIVE methods
1346
- const syncFromRPC = async () => {
1347
- if (this.syncState.syncing) return; // Already syncing
1365
+ // Fetch and add a specific block
1366
+ const fetchBlock = async (blockNum) => {
1367
+ const blockHex = '0x' + blockNum.toString(16);
1368
+ const block = await rpcCall('jaelis_getBlockByNumber', [blockHex, true]);
1369
+ if (block && this.blockchain?.addBlock) {
1370
+ await this.blockchain.addBlock(block);
1371
+ this.syncState.blocksDownloaded++;
1372
+ return true;
1373
+ }
1374
+ return false;
1375
+ };
1376
+
1377
+ // Sync missing blocks (used for both initial sync and catching up)
1378
+ const syncMissingBlocks = async () => {
1379
+ if (this.syncState.syncing) return;
1348
1380
  this.syncState.syncing = true;
1349
1381
 
1350
1382
  try {
1351
- // 1. Get remote block height using NATIVE jaelis_blockNumber
1352
1383
  const remoteHeightHex = await rpcCall('jaelis_blockNumber');
1353
1384
  this.syncState.remoteHeight = parseInt(remoteHeightHex, 16);
1354
-
1355
- // 2. Get local block height
1356
1385
  this.syncState.localHeight = this.blockchain?.getHeight?.() || 0;
1357
1386
 
1358
- // 3. Sync missing blocks (batch of 10 at a time)
1359
- const blocksToSync = Math.min(10, this.syncState.remoteHeight - this.syncState.localHeight);
1387
+ // Loop until fully synced
1388
+ while (this.syncState.localHeight < this.syncState.remoteHeight) {
1389
+ const blocksToSync = this.syncState.remoteHeight - this.syncState.localHeight;
1360
1390
 
1361
- if (blocksToSync > 0) {
1362
- console.log(`[SYNC] Syncing blocks ${this.syncState.localHeight + 1} to ${this.syncState.localHeight + blocksToSync} (remote: ${this.syncState.remoteHeight})`);
1391
+ // Batch size: larger during initial sync, smaller when near tip
1392
+ const batchSize = blocksToSync > 50 ? 25 : (blocksToSync > 10 ? 10 : blocksToSync);
1363
1393
 
1364
- for (let i = 1; i <= blocksToSync; i++) {
1365
- const blockNum = this.syncState.localHeight + i;
1366
- const blockHex = '0x' + blockNum.toString(16);
1394
+ if (blocksToSync > 1) {
1395
+ console.log(`[SYNC] Syncing ${batchSize} blocks (${this.syncState.localHeight + 1} → ${this.syncState.localHeight + batchSize})`);
1396
+ }
1367
1397
 
1368
- // Fetch full block with transactions using NATIVE jaelis_getBlockByNumber
1369
- // This returns JAELIS-native fields (lodeUsed, consensus: PoEC, etc.)
1370
- const block = await rpcCall('jaelis_getBlockByNumber', [blockHex, true]);
1398
+ for (let i = 1; i <= batchSize; i++) {
1399
+ await fetchBlock(this.syncState.localHeight + i);
1400
+ }
1371
1401
 
1372
- if (block) {
1373
- // Store block in local blockchain
1374
- if (this.blockchain?.addBlock) {
1375
- await this.blockchain.addBlock(block);
1376
- }
1377
- this.syncState.blocksDownloaded++;
1378
- }
1402
+ this.syncState.localHeight += batchSize;
1403
+
1404
+ if (batchSize > 1) {
1405
+ console.log(`[SYNC] Local height: ${this.syncState.localHeight}/${this.syncState.remoteHeight}`);
1379
1406
  }
1380
1407
 
1381
- this.syncState.localHeight += blocksToSync;
1382
- console.log(`[SYNC] Downloaded ${blocksToSync} blocks. Local height: ${this.syncState.localHeight}`);
1408
+ // Re-check remote height in case new blocks arrived during sync
1409
+ const newRemoteHex = await rpcCall('jaelis_blockNumber');
1410
+ this.syncState.remoteHeight = parseInt(newRemoteHex, 16);
1383
1411
  }
1384
1412
 
1385
1413
  this.syncState.lastSync = Date.now();
@@ -1392,20 +1420,167 @@ class EmbeddedNetwork {
1392
1420
  }
1393
1421
  };
1394
1422
 
1395
- // Initial sync
1396
- console.log('[SYNC] Starting RPC fallback sync from', this.remoteRpc);
1397
- await syncFromRPC();
1423
+ // ============================================================
1424
+ // WEBSOCKET SUBSCRIPTION (PREFERRED - like Ethereum newHeads)
1425
+ // ============================================================
1426
+ const tryWebSocketSync = async () => {
1427
+ try {
1428
+ const WebSocket = require('ws');
1429
+ const wsUrl = this.remoteRpc.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
1430
+
1431
+ console.log(`[SYNC] Trying WebSocket subscription at ${wsUrl}`);
1432
+
1433
+ return new Promise((resolve, reject) => {
1434
+ const ws = new WebSocket(wsUrl, {
1435
+ headers: { 'User-Agent': 'JAELIS-Node/1.8.0' }
1436
+ });
1437
+
1438
+ const timeout = setTimeout(() => {
1439
+ ws.close();
1440
+ reject(new Error('WebSocket connection timeout'));
1441
+ }, 10000);
1442
+
1443
+ ws.on('open', () => {
1444
+ clearTimeout(timeout);
1445
+ console.log('[SYNC] WebSocket connected! Subscribing to newHeads...');
1446
+
1447
+ // Subscribe to new block headers
1448
+ ws.send(JSON.stringify({
1449
+ jsonrpc: '2.0',
1450
+ method: 'jaelis_subscribe',
1451
+ params: ['newHeads'],
1452
+ id: 1
1453
+ }));
1454
+
1455
+ this.syncState.mode = 'websocket';
1456
+ this.wsConnection = ws;
1457
+ resolve(true);
1458
+ });
1459
+
1460
+ ws.on('message', async (data) => {
1461
+ try {
1462
+ const msg = JSON.parse(data.toString());
1463
+
1464
+ // Subscription confirmation
1465
+ if (msg.id === 1 && msg.result) {
1466
+ console.log(`[SYNC] ✓ Subscribed to newHeads (id: ${msg.result})`);
1467
+ console.log('[SYNC] Listening for new blocks via WebSocket...');
1468
+ return;
1469
+ }
1470
+
1471
+ // New block notification
1472
+ if (msg.method === 'jaelis_subscription' && msg.params?.result) {
1473
+ const header = msg.params.result;
1474
+ const blockNum = typeof header.number === 'string'
1475
+ ? parseInt(header.number, 16)
1476
+ : header.number;
1477
+
1478
+ // Fetch and sync the new block
1479
+ if (blockNum > this.syncState.localHeight) {
1480
+ // If we're behind, catch up first
1481
+ await syncMissingBlocks();
1482
+ }
1483
+
1484
+ this.syncState.remoteHeight = blockNum;
1485
+ this.emit('newBlock', header);
1486
+ }
1487
+ } catch (e) {
1488
+ // Ignore parse errors
1489
+ }
1490
+ });
1491
+
1492
+ ws.on('error', (err) => {
1493
+ clearTimeout(timeout);
1494
+ reject(err);
1495
+ });
1496
+
1497
+ ws.on('close', () => {
1498
+ console.log('[SYNC] WebSocket disconnected, falling back to polling...');
1499
+ this.syncState.mode = 'polling';
1500
+ this.wsConnection = null;
1501
+ // Fall back to polling
1502
+ startPolling();
1503
+ });
1504
+ });
1505
+ } catch (err) {
1506
+ // WebSocket module not available or connection failed
1507
+ return false;
1508
+ }
1509
+ };
1510
+
1511
+ // ============================================================
1512
+ // HTTP POLLING FALLBACK (like traditional Geth sync)
1513
+ // ============================================================
1514
+ const SYNCED_INTERVAL = 6000; // Poll every 6s when synced (2x block time)
1515
+ const CATCHUP_INTERVAL = 500; // Fast poll during catchup
1516
+
1517
+ const startPolling = () => {
1518
+ if (this.syncInterval) return; // Already polling
1519
+
1520
+ console.log('[SYNC] Using HTTP polling mode');
1521
+ this.syncState.mode = 'polling';
1522
+
1523
+ let pollInterval = CATCHUP_INTERVAL;
1524
+ let lastHeight = 0;
1525
+
1526
+ const poll = async () => {
1527
+ await syncMissingBlocks();
1528
+
1529
+ // Adjust polling speed based on sync status
1530
+ const isSynced = this.syncState.localHeight >= this.syncState.remoteHeight;
1531
+ const heightChanged = this.syncState.remoteHeight !== lastHeight;
1532
+
1533
+ if (isSynced && !heightChanged && pollInterval !== SYNCED_INTERVAL) {
1534
+ // Synced and no new blocks - slow down
1535
+ pollInterval = SYNCED_INTERVAL;
1536
+ clearInterval(this.syncInterval);
1537
+ this.syncInterval = setInterval(poll, pollInterval);
1538
+ console.log(`[SYNC] ✓ Synced at block ${this.syncState.localHeight}. Polling every ${pollInterval/1000}s`);
1539
+ } else if (!isSynced && pollInterval !== CATCHUP_INTERVAL) {
1540
+ // Falling behind - speed up
1541
+ pollInterval = CATCHUP_INTERVAL;
1542
+ clearInterval(this.syncInterval);
1543
+ this.syncInterval = setInterval(poll, pollInterval);
1544
+ }
1545
+
1546
+ lastHeight = this.syncState.remoteHeight;
1547
+ };
1548
+
1549
+ // Start polling
1550
+ this.syncInterval = setInterval(poll, pollInterval);
1551
+ };
1552
+
1553
+ // ============================================================
1554
+ // INITIAL SYNC
1555
+ // ============================================================
1556
+ console.log('[SYNC] Starting sync from', this.remoteRpc);
1557
+
1558
+ // Do initial block sync first
1559
+ await syncMissingBlocks();
1398
1560
 
1399
- // Continue syncing every 3 seconds (JAELIS has 3-second blocks)
1400
- this.syncInterval = setInterval(syncFromRPC, 3000);
1561
+ // Try WebSocket first (like Ethereum pub/sub), fall back to polling
1562
+ const wsSuccess = await tryWebSocketSync().catch(() => false);
1563
+
1564
+ if (!wsSuccess) {
1565
+ console.log('[SYNC] WebSocket unavailable, using HTTP polling');
1566
+ startPolling();
1567
+ }
1401
1568
  }
1402
1569
 
1403
1570
  async stop() {
1571
+ // Close WebSocket connection if active
1572
+ if (this.wsConnection) {
1573
+ this.wsConnection.close();
1574
+ this.wsConnection = null;
1575
+ }
1576
+ // Stop libp2p if active
1404
1577
  if (this.libp2p) {
1405
1578
  await this.libp2p.stop();
1406
1579
  }
1580
+ // Stop polling interval if active
1407
1581
  if (this.syncInterval) {
1408
1582
  clearInterval(this.syncInterval);
1583
+ this.syncInterval = null;
1409
1584
  }
1410
1585
  }
1411
1586
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jaelis-node",
3
- "version": "1.8.0",
4
- "description": "Official JAELIS Blockchain Node - Universal VM (6 languages), LevelDB state persistence, native jaelis_* RPC, multi-ecosystem compatibility (eth/solana/move/ton/btc/wasm/starknet), Cross-Chain Settlement (30+ chains!), AI-native MCP integration",
3
+ "version": "1.9.0",
4
+ "description": "Official JAELIS Blockchain Node - WebSocket real-time sync, Universal VM (6 languages), LevelDB state persistence, native jaelis_* RPC, multi-ecosystem compatibility (eth/solana/move/ton/btc/wasm/starknet), Cross-Chain Settlement (30+ chains!), AI-native MCP integration",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
7
7
  "jaelis-node": "./bin/jaelis-node.js"
@@ -72,7 +72,8 @@
72
72
  "express": "^4.18.2",
73
73
  "level": "^8.0.0",
74
74
  "libp2p": "^1.2.0",
75
- "ora": "^5.4.1"
75
+ "ora": "^5.4.1",
76
+ "ws": "^8.18.3"
76
77
  },
77
78
  "files": [
78
79
  "bin/",