jaelis-node 1.8.0 → 1.10.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,37 @@ 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
190
+ - **Auto-update checker** - Notifies when a new version is available on npm
191
+
192
+ **Update Notifications:**
193
+ ```
194
+ ╔════════════════════════════════════════════════════════════════╗
195
+ ║ 📦 UPDATE AVAILABLE ║
196
+ ╠════════════════════════════════════════════════════════════════╣
197
+ ║ Current: v1.9.0 → Latest: v1.10.0 ║
198
+ ║ ║
199
+ ║ Run: npm update -g jaelis-node ║
200
+ ║ Or: npx jaelis-node@latest start ║
201
+ ╚════════════════════════════════════════════════════════════════╝
202
+ ```
173
203
 
174
204
  ```bash
175
205
  # Check sync status
@@ -215,6 +245,42 @@ curl -X POST https://rpc.jaelis.io \
215
245
  }
216
246
  ```
217
247
 
248
+ ## WebSocket Subscriptions
249
+
250
+ Connect to the WebSocket endpoint for real-time updates:
251
+
252
+ ```javascript
253
+ const WebSocket = require('ws');
254
+ const ws = new WebSocket('wss://rpc.jaelis.io/ws');
255
+
256
+ ws.on('open', () => {
257
+ // Subscribe to new block headers
258
+ ws.send(JSON.stringify({
259
+ jsonrpc: '2.0',
260
+ method: 'jaelis_subscribe',
261
+ params: ['newHeads'],
262
+ id: 1
263
+ }));
264
+ });
265
+
266
+ ws.on('message', (data) => {
267
+ const msg = JSON.parse(data);
268
+ if (msg.method === 'jaelis_subscription') {
269
+ console.log('New block:', msg.params.result.number);
270
+ }
271
+ });
272
+ ```
273
+
274
+ **Available Subscriptions:**
275
+ | Subscription | Description |
276
+ |--------------|-------------|
277
+ | `newHeads` | New block headers as they're produced (real-time sync) |
278
+ | `newPendingTransactions` | Transaction hashes entering mempool |
279
+ | `logs` | Contract event logs (with filter options) |
280
+ | `syncing` | Sync status changes |
281
+
282
+ > **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.
283
+
218
284
  ## RPC Methods
219
285
 
220
286
  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
@@ -22,7 +22,7 @@
22
22
  * This follows Solana/Cosmos/Polkadot patterns for multi-ecosystem support.
23
23
  * ═══════════════════════════════════════════════════════════════════════════════
24
24
  *
25
- * @version 1.4.0
25
+ * @version 1.9.0
26
26
  * @author JAELIS Foundation
27
27
  */
28
28
 
@@ -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: {
@@ -506,6 +516,9 @@ class JaelisNode extends EventEmitter {
506
516
 
507
517
  console.log(`[JAELIS] Node started successfully`);
508
518
 
519
+ // Check for updates (non-blocking)
520
+ this._checkForUpdates();
521
+
509
522
  } catch (error) {
510
523
  console.error(`[JAELIS] Failed to start node: ${error.message}`);
511
524
  throw error;
@@ -729,12 +742,13 @@ class JaelisNode extends EventEmitter {
729
742
  const identity = this.walletConfig.getNodeIdentity();
730
743
  const rewardRecipient = this.walletConfig.getRewardRecipient();
731
744
 
745
+ const packageJson = require('../package.json');
732
746
  const nodeInfo = {
733
747
  nodeId: identity.id,
734
748
  publicKey: identity.publicKey,
735
749
  network: this.options.network,
736
750
  chainId: this.options.chainId,
737
- version: '1.3.0',
751
+ version: packageJson.version,
738
752
  rewardAddress: rewardRecipient?.address || null,
739
753
  rewardChainType: rewardRecipient?.type || null,
740
754
  capabilities: ['full-node', 'rpc'],
@@ -763,7 +777,7 @@ class JaelisNode extends EventEmitter {
763
777
  headers: {
764
778
  'Content-Type': 'application/json',
765
779
  'Content-Length': Buffer.byteLength(data),
766
- 'User-Agent': 'JAELIS-Node/1.7.0' // Identifies us as a node - gets priority treatment
780
+ 'User-Agent': `JAELIS-Node/${packageJson.version}` // Identifies us as a node - gets priority treatment
767
781
  }
768
782
  };
769
783
 
@@ -801,6 +815,7 @@ class JaelisNode extends EventEmitter {
801
815
  */
802
816
  _startHeartbeat() {
803
817
  const HEARTBEAT_INTERVAL = 60000; // 60 seconds
818
+ const packageJson = require('../package.json');
804
819
 
805
820
  this._heartbeatInterval = setInterval(async () => {
806
821
  if (!this.isRunning || !this._nodeInfo) return;
@@ -827,7 +842,7 @@ class JaelisNode extends EventEmitter {
827
842
  headers: {
828
843
  'Content-Type': 'application/json',
829
844
  'Content-Length': Buffer.byteLength(data),
830
- 'User-Agent': 'JAELIS-Node/1.7.0' // Identifies us as a node - gets priority treatment
845
+ 'User-Agent': `JAELIS-Node/${packageJson.version}` // Identifies us as a node - gets priority treatment
831
846
  }
832
847
  };
833
848
 
@@ -844,6 +859,79 @@ class JaelisNode extends EventEmitter {
844
859
  console.log('[JAELIS] Heartbeat started (60s interval)');
845
860
  }
846
861
 
862
+ /**
863
+ * Check for updates from npm registry
864
+ * Non-blocking - just notifies if a new version is available
865
+ */
866
+ async _checkForUpdates() {
867
+ try {
868
+ const https = require('https');
869
+ const packageJson = require('../package.json');
870
+ const currentVersion = packageJson.version;
871
+
872
+ const options = {
873
+ hostname: 'registry.npmjs.org',
874
+ path: '/jaelis-node/latest',
875
+ method: 'GET',
876
+ headers: {
877
+ 'Accept': 'application/json',
878
+ 'User-Agent': `JAELIS-Node/${currentVersion}`
879
+ },
880
+ timeout: 5000
881
+ };
882
+
883
+ const req = https.request(options, (res) => {
884
+ let body = '';
885
+ res.on('data', chunk => body += chunk);
886
+ res.on('end', () => {
887
+ try {
888
+ const data = JSON.parse(body);
889
+ const latestVersion = data.version;
890
+
891
+ if (latestVersion && this._isNewerVersion(currentVersion, latestVersion)) {
892
+ console.log('');
893
+ console.log('╔════════════════════════════════════════════════════════════════╗');
894
+ console.log('║ 📦 UPDATE AVAILABLE ║');
895
+ console.log('╠════════════════════════════════════════════════════════════════╣');
896
+ console.log(`║ Current: v${currentVersion.padEnd(10)} → Latest: v${latestVersion.padEnd(20)}║`);
897
+ console.log('║ ║');
898
+ console.log('║ Run: npm update -g jaelis-node ║');
899
+ console.log('║ Or: npx jaelis-node@latest start ║');
900
+ console.log('╚════════════════════════════════════════════════════════════════╝');
901
+ console.log('');
902
+ }
903
+ } catch (e) {
904
+ // Silent - update check is best-effort
905
+ }
906
+ });
907
+ });
908
+
909
+ req.on('error', () => {}); // Silent fail
910
+ req.on('timeout', () => req.destroy());
911
+ req.end();
912
+
913
+ } catch (error) {
914
+ // Update check is non-critical, fail silently
915
+ }
916
+ }
917
+
918
+ /**
919
+ * Compare semver versions
920
+ * @returns true if latest > current
921
+ */
922
+ _isNewerVersion(current, latest) {
923
+ const currentParts = current.split('.').map(Number);
924
+ const latestParts = latest.split('.').map(Number);
925
+
926
+ for (let i = 0; i < 3; i++) {
927
+ const c = currentParts[i] || 0;
928
+ const l = latestParts[i] || 0;
929
+ if (l > c) return true;
930
+ if (l < c) return false;
931
+ }
932
+ return false;
933
+ }
934
+
847
935
  /**
848
936
  * Stop heartbeat
849
937
  */
@@ -858,18 +946,40 @@ class JaelisNode extends EventEmitter {
858
946
  * Get node status
859
947
  */
860
948
  getStatus() {
949
+ const packageJson = require('../package.json');
950
+ const uptimeMs = this.startTime ? Date.now() - this.startTime : 0;
951
+
861
952
  return {
953
+ healthy: this.isRunning,
862
954
  running: this.isRunning,
955
+ version: packageJson.version,
863
956
  network: this.options.network,
864
957
  chainId: this.options.chainId,
865
- uptime: this.startTime ? Date.now() - this.startTime : 0,
958
+ uptime: uptimeMs,
959
+ uptimeHuman: this._formatUptime(uptimeMs),
866
960
  peers: this.peerCount,
867
961
  rpcPort: this.options.rpcPort,
868
962
  p2pPort: this.options.p2pPort,
869
963
  syncMode: this.options.syncMode,
870
- blockHeight: this.blockchain?.getHeight?.() || 0
964
+ blockHeight: this.blockchain?.getHeight?.() || 0,
965
+ nodeId: this._nodeInfo?.nodeId?.slice(0, 16) + '...' || null
871
966
  };
872
967
  }
968
+
969
+ /**
970
+ * Format uptime in human readable format
971
+ */
972
+ _formatUptime(ms) {
973
+ const seconds = Math.floor(ms / 1000);
974
+ const minutes = Math.floor(seconds / 60);
975
+ const hours = Math.floor(minutes / 60);
976
+ const days = Math.floor(hours / 24);
977
+
978
+ if (days > 0) return `${days}d ${hours % 24}h ${minutes % 60}m`;
979
+ if (hours > 0) return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
980
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
981
+ return `${seconds}s`;
982
+ }
873
983
  }
874
984
 
875
985
  /**
@@ -1204,8 +1314,9 @@ class EmbeddedBlockchain {
1204
1314
  }
1205
1315
  }
1206
1316
 
1207
- class EmbeddedNetwork {
1317
+ class EmbeddedNetwork extends EventEmitter {
1208
1318
  constructor(options = {}) {
1319
+ super();
1209
1320
  this.port = options.port || 30303;
1210
1321
  this.host = options.host || '0.0.0.0';
1211
1322
  this.maxPeers = options.maxPeers || 50;
@@ -1221,16 +1332,24 @@ class EmbeddedNetwork {
1221
1332
  }
1222
1333
 
1223
1334
  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}`);
1335
+ // ALWAYS start RPC sync first - this is reliable through Cloudflare
1336
+ // P2P is optional/additional for peer discovery
1337
+ console.log(`[NETWORK] Starting RPC sync from ${this.remoteRpc}`);
1338
+ await this._startRPCFallback();
1339
+ this.isRPCFallback = true;
1340
+
1341
+ // Try P2P alongside (optional - for peer discovery when bootstrap nodes available)
1342
+ if (this.bootstrapNodes && this.bootstrapNodes.length > 0) {
1343
+ try {
1344
+ await this._startLibp2p();
1345
+ this.isP2PConnected = true;
1346
+ console.log(`[NETWORK] P2P network also started on port ${this.port}`);
1347
+ } catch (err) {
1348
+ console.log(`[NETWORK] P2P unavailable (${err.message}), continuing with RPC only`);
1349
+ }
1350
+ } else {
1351
+ console.log(`[NETWORK] No bootstrap nodes configured - using RPC sync only`);
1352
+ console.log(`[NETWORK] This is fine! RPC sync is reliable and works through firewalls.`);
1234
1353
  }
1235
1354
  }
1236
1355
 
@@ -1296,7 +1415,8 @@ class EmbeddedNetwork {
1296
1415
  remoteHeight: 0,
1297
1416
  syncing: false,
1298
1417
  lastSync: null,
1299
- blocksDownloaded: 0
1418
+ blocksDownloaded: 0,
1419
+ mode: 'polling' // 'websocket' or 'polling'
1300
1420
  };
1301
1421
 
1302
1422
  // Helper to make RPC calls to remote node
@@ -1322,7 +1442,7 @@ class EmbeddedNetwork {
1322
1442
  headers: {
1323
1443
  'Content-Type': 'application/json',
1324
1444
  'Content-Length': Buffer.byteLength(data),
1325
- 'User-Agent': 'JAELIS-Node/1.7.0' // Node sync traffic - exempt from rate limits
1445
+ 'User-Agent': 'JAELIS-Node/1.8.0' // Node sync traffic
1326
1446
  }
1327
1447
  }, (res) => {
1328
1448
  let body = '';
@@ -1342,44 +1462,52 @@ class EmbeddedNetwork {
1342
1462
  });
1343
1463
  };
1344
1464
 
1345
- // Sync blocks from remote RPC using JAELIS NATIVE methods
1346
- const syncFromRPC = async () => {
1347
- if (this.syncState.syncing) return; // Already syncing
1465
+ // Fetch and add a specific block
1466
+ const fetchBlock = async (blockNum) => {
1467
+ const blockHex = '0x' + blockNum.toString(16);
1468
+ const block = await rpcCall('jaelis_getBlockByNumber', [blockHex, true]);
1469
+ if (block && this.blockchain?.addBlock) {
1470
+ await this.blockchain.addBlock(block);
1471
+ this.syncState.blocksDownloaded++;
1472
+ return true;
1473
+ }
1474
+ return false;
1475
+ };
1476
+
1477
+ // Sync missing blocks (used for both initial sync and catching up)
1478
+ const syncMissingBlocks = async () => {
1479
+ if (this.syncState.syncing) return;
1348
1480
  this.syncState.syncing = true;
1349
1481
 
1350
1482
  try {
1351
- // 1. Get remote block height using NATIVE jaelis_blockNumber
1352
1483
  const remoteHeightHex = await rpcCall('jaelis_blockNumber');
1353
1484
  this.syncState.remoteHeight = parseInt(remoteHeightHex, 16);
1354
-
1355
- // 2. Get local block height
1356
1485
  this.syncState.localHeight = this.blockchain?.getHeight?.() || 0;
1357
1486
 
1358
- // 3. Sync missing blocks (batch of 10 at a time)
1359
- const blocksToSync = Math.min(10, this.syncState.remoteHeight - this.syncState.localHeight);
1487
+ // Loop until fully synced
1488
+ while (this.syncState.localHeight < this.syncState.remoteHeight) {
1489
+ const blocksToSync = this.syncState.remoteHeight - this.syncState.localHeight;
1490
+
1491
+ // Batch size: larger during initial sync, smaller when near tip
1492
+ const batchSize = blocksToSync > 50 ? 25 : (blocksToSync > 10 ? 10 : blocksToSync);
1360
1493
 
1361
- if (blocksToSync > 0) {
1362
- console.log(`[SYNC] Syncing blocks ${this.syncState.localHeight + 1} to ${this.syncState.localHeight + blocksToSync} (remote: ${this.syncState.remoteHeight})`);
1494
+ if (blocksToSync > 1) {
1495
+ console.log(`[SYNC] Syncing ${batchSize} blocks (${this.syncState.localHeight + 1} ${this.syncState.localHeight + batchSize})`);
1496
+ }
1363
1497
 
1364
- for (let i = 1; i <= blocksToSync; i++) {
1365
- const blockNum = this.syncState.localHeight + i;
1366
- const blockHex = '0x' + blockNum.toString(16);
1498
+ for (let i = 1; i <= batchSize; i++) {
1499
+ await fetchBlock(this.syncState.localHeight + i);
1500
+ }
1367
1501
 
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]);
1502
+ this.syncState.localHeight += batchSize;
1371
1503
 
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
- }
1504
+ if (batchSize > 1) {
1505
+ console.log(`[SYNC] Local height: ${this.syncState.localHeight}/${this.syncState.remoteHeight}`);
1379
1506
  }
1380
1507
 
1381
- this.syncState.localHeight += blocksToSync;
1382
- console.log(`[SYNC] Downloaded ${blocksToSync} blocks. Local height: ${this.syncState.localHeight}`);
1508
+ // Re-check remote height in case new blocks arrived during sync
1509
+ const newRemoteHex = await rpcCall('jaelis_blockNumber');
1510
+ this.syncState.remoteHeight = parseInt(newRemoteHex, 16);
1383
1511
  }
1384
1512
 
1385
1513
  this.syncState.lastSync = Date.now();
@@ -1392,20 +1520,167 @@ class EmbeddedNetwork {
1392
1520
  }
1393
1521
  };
1394
1522
 
1395
- // Initial sync
1396
- console.log('[SYNC] Starting RPC fallback sync from', this.remoteRpc);
1397
- await syncFromRPC();
1523
+ // ============================================================
1524
+ // WEBSOCKET SUBSCRIPTION (PREFERRED - like Ethereum newHeads)
1525
+ // ============================================================
1526
+ const tryWebSocketSync = async () => {
1527
+ try {
1528
+ const WebSocket = require('ws');
1529
+ const wsUrl = this.remoteRpc.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
1530
+
1531
+ console.log(`[SYNC] Trying WebSocket subscription at ${wsUrl}`);
1532
+
1533
+ return new Promise((resolve, reject) => {
1534
+ const ws = new WebSocket(wsUrl, {
1535
+ headers: { 'User-Agent': 'JAELIS-Node/1.8.0' }
1536
+ });
1537
+
1538
+ const timeout = setTimeout(() => {
1539
+ ws.close();
1540
+ reject(new Error('WebSocket connection timeout'));
1541
+ }, 10000);
1542
+
1543
+ ws.on('open', () => {
1544
+ clearTimeout(timeout);
1545
+ console.log('[SYNC] WebSocket connected! Subscribing to newHeads...');
1546
+
1547
+ // Subscribe to new block headers
1548
+ ws.send(JSON.stringify({
1549
+ jsonrpc: '2.0',
1550
+ method: 'jaelis_subscribe',
1551
+ params: ['newHeads'],
1552
+ id: 1
1553
+ }));
1554
+
1555
+ this.syncState.mode = 'websocket';
1556
+ this.wsConnection = ws;
1557
+ resolve(true);
1558
+ });
1559
+
1560
+ ws.on('message', async (data) => {
1561
+ try {
1562
+ const msg = JSON.parse(data.toString());
1563
+
1564
+ // Subscription confirmation
1565
+ if (msg.id === 1 && msg.result) {
1566
+ console.log(`[SYNC] ✓ Subscribed to newHeads (id: ${msg.result})`);
1567
+ console.log('[SYNC] Listening for new blocks via WebSocket...');
1568
+ return;
1569
+ }
1570
+
1571
+ // New block notification
1572
+ if (msg.method === 'jaelis_subscription' && msg.params?.result) {
1573
+ const header = msg.params.result;
1574
+ const blockNum = typeof header.number === 'string'
1575
+ ? parseInt(header.number, 16)
1576
+ : header.number;
1577
+
1578
+ // Fetch and sync the new block
1579
+ if (blockNum > this.syncState.localHeight) {
1580
+ // If we're behind, catch up first
1581
+ await syncMissingBlocks();
1582
+ }
1583
+
1584
+ this.syncState.remoteHeight = blockNum;
1585
+ this.emit('newBlock', header);
1586
+ }
1587
+ } catch (e) {
1588
+ // Ignore parse errors
1589
+ }
1590
+ });
1398
1591
 
1399
- // Continue syncing every 3 seconds (JAELIS has 3-second blocks)
1400
- this.syncInterval = setInterval(syncFromRPC, 3000);
1592
+ ws.on('error', (err) => {
1593
+ clearTimeout(timeout);
1594
+ reject(err);
1595
+ });
1596
+
1597
+ ws.on('close', () => {
1598
+ console.log('[SYNC] WebSocket disconnected, falling back to polling...');
1599
+ this.syncState.mode = 'polling';
1600
+ this.wsConnection = null;
1601
+ // Fall back to polling
1602
+ startPolling();
1603
+ });
1604
+ });
1605
+ } catch (err) {
1606
+ // WebSocket module not available or connection failed
1607
+ return false;
1608
+ }
1609
+ };
1610
+
1611
+ // ============================================================
1612
+ // HTTP POLLING FALLBACK (like traditional Geth sync)
1613
+ // ============================================================
1614
+ const SYNCED_INTERVAL = 6000; // Poll every 6s when synced (2x block time)
1615
+ const CATCHUP_INTERVAL = 500; // Fast poll during catchup
1616
+
1617
+ const startPolling = () => {
1618
+ if (this.syncInterval) return; // Already polling
1619
+
1620
+ console.log('[SYNC] Using HTTP polling mode');
1621
+ this.syncState.mode = 'polling';
1622
+
1623
+ let pollInterval = CATCHUP_INTERVAL;
1624
+ let lastHeight = 0;
1625
+
1626
+ const poll = async () => {
1627
+ await syncMissingBlocks();
1628
+
1629
+ // Adjust polling speed based on sync status
1630
+ const isSynced = this.syncState.localHeight >= this.syncState.remoteHeight;
1631
+ const heightChanged = this.syncState.remoteHeight !== lastHeight;
1632
+
1633
+ if (isSynced && !heightChanged && pollInterval !== SYNCED_INTERVAL) {
1634
+ // Synced and no new blocks - slow down
1635
+ pollInterval = SYNCED_INTERVAL;
1636
+ clearInterval(this.syncInterval);
1637
+ this.syncInterval = setInterval(poll, pollInterval);
1638
+ console.log(`[SYNC] ✓ Synced at block ${this.syncState.localHeight}. Polling every ${pollInterval/1000}s`);
1639
+ } else if (!isSynced && pollInterval !== CATCHUP_INTERVAL) {
1640
+ // Falling behind - speed up
1641
+ pollInterval = CATCHUP_INTERVAL;
1642
+ clearInterval(this.syncInterval);
1643
+ this.syncInterval = setInterval(poll, pollInterval);
1644
+ }
1645
+
1646
+ lastHeight = this.syncState.remoteHeight;
1647
+ };
1648
+
1649
+ // Start polling
1650
+ this.syncInterval = setInterval(poll, pollInterval);
1651
+ };
1652
+
1653
+ // ============================================================
1654
+ // INITIAL SYNC
1655
+ // ============================================================
1656
+ console.log('[SYNC] Starting sync from', this.remoteRpc);
1657
+
1658
+ // Do initial block sync first
1659
+ await syncMissingBlocks();
1660
+
1661
+ // Try WebSocket first (like Ethereum pub/sub), fall back to polling
1662
+ const wsSuccess = await tryWebSocketSync().catch(() => false);
1663
+
1664
+ if (!wsSuccess) {
1665
+ console.log('[SYNC] WebSocket unavailable, using HTTP polling');
1666
+ startPolling();
1667
+ }
1401
1668
  }
1402
1669
 
1403
1670
  async stop() {
1671
+ // Close WebSocket connection if active
1672
+ if (this.wsConnection) {
1673
+ this.wsConnection.close();
1674
+ this.wsConnection = null;
1675
+ }
1676
+ // Stop libp2p if active
1404
1677
  if (this.libp2p) {
1405
1678
  await this.libp2p.stop();
1406
1679
  }
1680
+ // Stop polling interval if active
1407
1681
  if (this.syncInterval) {
1408
1682
  clearInterval(this.syncInterval);
1683
+ this.syncInterval = null;
1409
1684
  }
1410
1685
  }
1411
1686
 
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.10.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/",