jaelis-node 1.7.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:
@@ -440,6 +493,22 @@ JAELIS implements three patent-pending systems:
440
493
  - Address poisoning detection
441
494
  - No trusted setup for privacy features
442
495
 
496
+ ## AI Integration (MCP)
497
+
498
+ JAELIS is AI-native. Claude Desktop users can interact with the blockchain through MCP (Model Context Protocol):
499
+
500
+ ```json
501
+ {
502
+ "mcpServers": {
503
+ "jaelis": {
504
+ "url": "https://mcp.jaelis.io/sse"
505
+ }
506
+ }
507
+ }
508
+ ```
509
+
510
+ Add this to your Claude Desktop config and get 274+ JAELIS tools automatically - create wallets, send transactions, deploy contracts, query balances, and more.
511
+
443
512
  ## Links
444
513
 
445
514
  - SDK: [npmjs.com/package/jaelis.js](https://www.npmjs.com/package/jaelis.js)
@@ -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',
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)
45
60
  bootstrapNodes: [
46
- '/dns4/rpc.jaelis.io/tcp/30305/p2p/QmBootstrap1',
47
- '/dns4/rpc.jaelis.io/tcp/30306/p2p/QmBootstrap2',
48
- '/dns4/rpc.jaelis.io/tcp/30307/p2p/QmBootstrap3'
61
+ // Will be: '/dns4/rpc.jaelis.io/tcp/30304/p2p/PEER_ID'
49
62
  ]
50
63
  },
51
64
  mainnet: {
@@ -8,9 +8,7 @@
8
8
  "name": "testnet",
9
9
  "chainId": 4545,
10
10
  "bootstrapNodes": [
11
- "/dns4/rpc.jaelis.io/tcp/30305/p2p/QmBootstrap1",
12
- "/dns4/rpc.jaelis.io/tcp/30306/p2p/QmBootstrap2",
13
- "/dns4/rpc.jaelis.io/tcp/30307/p2p/QmBootstrap3"
11
+ "/dns4/rpc.jaelis.io/tcp/30303"
14
12
  ]
15
13
  },
16
14
  "rpc": {
@@ -8,9 +8,7 @@
8
8
  "https://rpc.jaelis.io"
9
9
  ],
10
10
  "bootstrapNodes": [
11
- "/dns4/rpc.jaelis.io/tcp/30305/p2p/QmBootstrap1",
12
- "/dns4/rpc.jaelis.io/tcp/30306/p2p/QmBootstrap2",
13
- "/dns4/rpc.jaelis.io/tcp/30307/p2p/QmBootstrap3"
11
+ "/dns4/rpc.jaelis.io/tcp/30303"
14
12
  ],
15
13
  "genesis": {
16
14
  "timestamp": 1731398400000,
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: {
@@ -762,7 +772,8 @@ class JaelisNode extends EventEmitter {
762
772
  method: 'POST',
763
773
  headers: {
764
774
  'Content-Type': 'application/json',
765
- 'Content-Length': Buffer.byteLength(data)
775
+ 'Content-Length': Buffer.byteLength(data),
776
+ 'User-Agent': 'JAELIS-Node/1.7.0' // Identifies us as a node - gets priority treatment
766
777
  }
767
778
  };
768
779
 
@@ -825,7 +836,8 @@ class JaelisNode extends EventEmitter {
825
836
  method: 'POST',
826
837
  headers: {
827
838
  'Content-Type': 'application/json',
828
- 'Content-Length': Buffer.byteLength(data)
839
+ 'Content-Length': Buffer.byteLength(data),
840
+ 'User-Agent': 'JAELIS-Node/1.7.0' // Identifies us as a node - gets priority treatment
829
841
  }
830
842
  };
831
843
 
@@ -1102,23 +1114,46 @@ class EmbeddedBlockchain {
1102
1114
  * Validates the block before adding to chain
1103
1115
  */
1104
1116
  async addBlock(block) {
1105
- if (!block || !block.number) {
1117
+ if (!block || block.number === undefined) {
1106
1118
  throw new Error('Invalid block');
1107
1119
  }
1108
1120
 
1121
+ // Normalize block number - could be hex string (0x1) or decimal
1122
+ let blockNumber = block.number;
1123
+ if (typeof blockNumber === 'string') {
1124
+ if (blockNumber.startsWith('0x')) {
1125
+ blockNumber = parseInt(blockNumber, 16);
1126
+ } else {
1127
+ blockNumber = parseInt(blockNumber, 10);
1128
+ }
1129
+ }
1130
+
1109
1131
  const expectedNumber = this.chain.length;
1110
- if (block.number !== expectedNumber) {
1132
+ if (blockNumber !== expectedNumber) {
1111
1133
  // Skip if we already have this block or it's out of order
1112
- if (block.number < expectedNumber) {
1134
+ if (blockNumber < expectedNumber) {
1113
1135
  return { status: 'skipped', reason: 'already_have' };
1114
1136
  }
1115
- throw new Error(`Block out of order: expected ${expectedNumber}, got ${block.number}`);
1137
+ throw new Error(`Block out of order: expected ${expectedNumber}, got ${blockNumber}`);
1116
1138
  }
1117
1139
 
1118
- // Validate parent hash
1140
+ // Store normalized block number for consistency
1141
+ block.number = blockNumber;
1142
+
1143
+ // Validate parent hash (skip for first sync - genesis hash may differ)
1144
+ // Like Ethereum "fast sync" - we trust the RPC source initially
1119
1145
  const parentBlock = this.getLatestBlock();
1120
- if (block.parentHash && parentBlock.hash && block.parentHash !== parentBlock.hash) {
1121
- throw new Error('Invalid parent hash - possible chain reorg');
1146
+ if (blockNumber > 1 && block.parentHash && parentBlock.hash) {
1147
+ // Only validate parent hash for blocks after genesis
1148
+ // During initial sync, parent hashes are validated by the remote node
1149
+ if (block.parentHash !== parentBlock.hash) {
1150
+ // Check if this is initial sync (chain is mostly empty)
1151
+ if (this.chain.length > 10) {
1152
+ throw new Error('Invalid parent hash - possible chain reorg');
1153
+ }
1154
+ // During initial sync, log but continue
1155
+ console.log(`[SYNC] Accepting block ${blockNumber} (trusting remote during initial sync)`);
1156
+ }
1122
1157
  }
1123
1158
 
1124
1159
  // Add block to chain
@@ -1179,8 +1214,9 @@ class EmbeddedBlockchain {
1179
1214
  }
1180
1215
  }
1181
1216
 
1182
- class EmbeddedNetwork {
1217
+ class EmbeddedNetwork extends EventEmitter {
1183
1218
  constructor(options = {}) {
1219
+ super();
1184
1220
  this.port = options.port || 30303;
1185
1221
  this.host = options.host || '0.0.0.0';
1186
1222
  this.maxPeers = options.maxPeers || 50;
@@ -1196,16 +1232,24 @@ class EmbeddedNetwork {
1196
1232
  }
1197
1233
 
1198
1234
  async start() {
1199
- // Try libp2p first, fallback to RPC sync
1200
- try {
1201
- await this._startLibp2p();
1202
- this.isP2PConnected = true;
1203
- console.log(`[NETWORK] libp2p P2P network started on port ${this.port}`);
1204
- } catch (err) {
1205
- console.log(`[NETWORK] P2P failed (${err.message}), using RPC fallback`);
1206
- await this._startRPCFallback();
1207
- this.isRPCFallback = true;
1208
- 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.`);
1209
1253
  }
1210
1254
  }
1211
1255
 
@@ -1215,9 +1259,10 @@ class EmbeddedNetwork {
1215
1259
  const { tcp } = await import('@libp2p/tcp');
1216
1260
  const { noise } = await import('@chainsafe/libp2p-noise');
1217
1261
  const { yamux } = await import('@chainsafe/libp2p-yamux');
1218
- const { bootstrap } = await import('@libp2p/bootstrap');
1219
1262
  const { identify } = await import('@libp2p/identify');
1220
1263
 
1264
+ // Create libp2p node - JAELIS user node
1265
+ // Like Ethereum: start listening, bootstrap nodes come later via DHT
1221
1266
  const config = {
1222
1267
  addresses: {
1223
1268
  listen: [`/ip4/${this.host}/tcp/${this.port}`]
@@ -1230,17 +1275,11 @@ class EmbeddedNetwork {
1230
1275
  }
1231
1276
  };
1232
1277
 
1233
- // Add bootstrap if we have nodes
1234
- if (this.bootstrapNodes.length > 0) {
1235
- const validBootstrap = this.bootstrapNodes.filter(n => n.includes('/p2p/'));
1236
- if (validBootstrap.length > 0) {
1237
- config.peerDiscovery = [bootstrap({ list: validBootstrap })];
1238
- }
1239
- }
1240
-
1241
1278
  this.libp2p = await createLibp2p(config);
1242
1279
  await this.libp2p.start();
1243
1280
 
1281
+ console.log(`[P2P] Node started with ID: ${this.libp2p.peerId.toString().slice(0, 16)}...`);
1282
+
1244
1283
  // Track peers
1245
1284
  this.libp2p.addEventListener('peer:connect', (evt) => {
1246
1285
  const peerId = evt.detail.toString();
@@ -1253,6 +1292,20 @@ class EmbeddedNetwork {
1253
1292
  this.peers.delete(peerId);
1254
1293
  console.log(`[P2P] Peer disconnected (${this.peers.size} remaining)`);
1255
1294
  });
1295
+
1296
+ // Try to dial bootstrap nodes directly (without peer ID)
1297
+ // Like Ethereum: we connect via TCP, then learn peer ID during handshake
1298
+ for (const addr of this.bootstrapNodes) {
1299
+ try {
1300
+ const { multiaddr } = await import('@multiformats/multiaddr');
1301
+ const ma = multiaddr(addr);
1302
+ await this.libp2p.dial(ma);
1303
+ console.log(`[P2P] Connected to bootstrap: ${addr}`);
1304
+ } catch (err) {
1305
+ // Bootstrap unavailable - this is OK, we'll use RPC fallback
1306
+ console.log(`[P2P] Bootstrap ${addr.slice(0, 30)}... unavailable`);
1307
+ }
1308
+ }
1256
1309
  }
1257
1310
 
1258
1311
  async _startRPCFallback() {
@@ -1262,7 +1315,8 @@ class EmbeddedNetwork {
1262
1315
  remoteHeight: 0,
1263
1316
  syncing: false,
1264
1317
  lastSync: null,
1265
- blocksDownloaded: 0
1318
+ blocksDownloaded: 0,
1319
+ mode: 'polling' // 'websocket' or 'polling'
1266
1320
  };
1267
1321
 
1268
1322
  // Helper to make RPC calls to remote node
@@ -1287,7 +1341,8 @@ class EmbeddedNetwork {
1287
1341
  method: 'POST',
1288
1342
  headers: {
1289
1343
  'Content-Type': 'application/json',
1290
- 'Content-Length': Buffer.byteLength(data)
1344
+ 'Content-Length': Buffer.byteLength(data),
1345
+ 'User-Agent': 'JAELIS-Node/1.8.0' // Node sync traffic
1291
1346
  }
1292
1347
  }, (res) => {
1293
1348
  let body = '';
@@ -1307,44 +1362,52 @@ class EmbeddedNetwork {
1307
1362
  });
1308
1363
  };
1309
1364
 
1310
- // Sync blocks from remote RPC using JAELIS NATIVE methods
1311
- const syncFromRPC = async () => {
1312
- 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;
1313
1380
  this.syncState.syncing = true;
1314
1381
 
1315
1382
  try {
1316
- // 1. Get remote block height using NATIVE jaelis_blockNumber
1317
1383
  const remoteHeightHex = await rpcCall('jaelis_blockNumber');
1318
1384
  this.syncState.remoteHeight = parseInt(remoteHeightHex, 16);
1319
-
1320
- // 2. Get local block height
1321
1385
  this.syncState.localHeight = this.blockchain?.getHeight?.() || 0;
1322
1386
 
1323
- // 3. Sync missing blocks (batch of 10 at a time)
1324
- 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;
1325
1390
 
1326
- if (blocksToSync > 0) {
1327
- 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);
1328
1393
 
1329
- for (let i = 1; i <= blocksToSync; i++) {
1330
- const blockNum = this.syncState.localHeight + i;
1331
- 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
+ }
1332
1397
 
1333
- // Fetch full block with transactions using NATIVE jaelis_getBlockByNumber
1334
- // This returns JAELIS-native fields (lodeUsed, consensus: PoEC, etc.)
1335
- const block = await rpcCall('jaelis_getBlockByNumber', [blockHex, true]);
1398
+ for (let i = 1; i <= batchSize; i++) {
1399
+ await fetchBlock(this.syncState.localHeight + i);
1400
+ }
1336
1401
 
1337
- if (block) {
1338
- // Store block in local blockchain
1339
- if (this.blockchain?.addBlock) {
1340
- await this.blockchain.addBlock(block);
1341
- }
1342
- this.syncState.blocksDownloaded++;
1343
- }
1402
+ this.syncState.localHeight += batchSize;
1403
+
1404
+ if (batchSize > 1) {
1405
+ console.log(`[SYNC] Local height: ${this.syncState.localHeight}/${this.syncState.remoteHeight}`);
1344
1406
  }
1345
1407
 
1346
- this.syncState.localHeight += blocksToSync;
1347
- 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);
1348
1411
  }
1349
1412
 
1350
1413
  this.syncState.lastSync = Date.now();
@@ -1357,20 +1420,167 @@ class EmbeddedNetwork {
1357
1420
  }
1358
1421
  };
1359
1422
 
1360
- // Initial sync
1361
- console.log('[SYNC] Starting RPC fallback sync from', this.remoteRpc);
1362
- 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);
1363
1557
 
1364
- // Continue syncing every 3 seconds (JAELIS has 3-second blocks)
1365
- this.syncInterval = setInterval(syncFromRPC, 3000);
1558
+ // Do initial block sync first
1559
+ await syncMissingBlocks();
1560
+
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
+ }
1366
1568
  }
1367
1569
 
1368
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
1369
1577
  if (this.libp2p) {
1370
1578
  await this.libp2p.stop();
1371
1579
  }
1580
+ // Stop polling interval if active
1372
1581
  if (this.syncInterval) {
1373
1582
  clearInterval(this.syncInterval);
1583
+ this.syncInterval = null;
1374
1584
  }
1375
1585
  }
1376
1586
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jaelis-node",
3
- "version": "1.7.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!)",
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"
@@ -60,18 +60,20 @@
60
60
  "node": ">=18.0.0"
61
61
  },
62
62
  "dependencies": {
63
+ "@chainsafe/libp2p-noise": "^15.1.2",
64
+ "@chainsafe/libp2p-yamux": "^6.0.2",
65
+ "@libp2p/bootstrap": "^10.1.5",
66
+ "@libp2p/identify": "^1.0.21",
67
+ "@libp2p/tcp": "^9.1.6",
68
+ "@multiformats/multiaddr": "^13.0.1",
69
+ "chalk": "^4.1.2",
70
+ "commander": "^11.1.0",
71
+ "cors": "^2.8.5",
72
+ "express": "^4.18.2",
63
73
  "level": "^8.0.0",
64
74
  "libp2p": "^1.2.0",
65
- "@libp2p/tcp": "^9.0.0",
66
- "@libp2p/bootstrap": "^10.0.0",
67
- "@libp2p/identify": "^1.0.0",
68
- "@chainsafe/libp2p-noise": "^15.0.0",
69
- "@chainsafe/libp2p-yamux": "^6.0.0",
70
- "express": "^4.18.2",
71
- "cors": "^2.8.5",
72
- "commander": "^11.1.0",
73
- "chalk": "^4.1.2",
74
- "ora": "^5.4.1"
75
+ "ora": "^5.4.1",
76
+ "ws": "^8.18.3"
75
77
  },
76
78
  "files": [
77
79
  "bin/",