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 +70 -1
- package/bin/jaelis-node.js +16 -3
- package/config/default.json +1 -3
- package/config/testnet.json +1 -3
- package/lib/index.js +272 -62
- package/package.json +14 -12
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)
|
package/bin/jaelis-node.js
CHANGED
|
@@ -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/
|
|
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: {
|
package/config/default.json
CHANGED
|
@@ -8,9 +8,7 @@
|
|
|
8
8
|
"name": "testnet",
|
|
9
9
|
"chainId": 4545,
|
|
10
10
|
"bootstrapNodes": [
|
|
11
|
-
"/dns4/rpc.jaelis.io/tcp/
|
|
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": {
|
package/config/testnet.json
CHANGED
|
@@ -8,9 +8,7 @@
|
|
|
8
8
|
"https://rpc.jaelis.io"
|
|
9
9
|
],
|
|
10
10
|
"bootstrapNodes": [
|
|
11
|
-
"/dns4/rpc.jaelis.io/tcp/
|
|
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
|
-
|
|
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 ||
|
|
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 (
|
|
1132
|
+
if (blockNumber !== expectedNumber) {
|
|
1111
1133
|
// Skip if we already have this block or it's out of order
|
|
1112
|
-
if (
|
|
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 ${
|
|
1137
|
+
throw new Error(`Block out of order: expected ${expectedNumber}, got ${blockNumber}`);
|
|
1116
1138
|
}
|
|
1117
1139
|
|
|
1118
|
-
//
|
|
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 (
|
|
1121
|
-
|
|
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
|
-
//
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
-
//
|
|
1311
|
-
const
|
|
1312
|
-
|
|
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
|
-
//
|
|
1324
|
-
|
|
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
|
-
|
|
1327
|
-
|
|
1391
|
+
// Batch size: larger during initial sync, smaller when near tip
|
|
1392
|
+
const batchSize = blocksToSync > 50 ? 25 : (blocksToSync > 10 ? 10 : blocksToSync);
|
|
1328
1393
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1394
|
+
if (blocksToSync > 1) {
|
|
1395
|
+
console.log(`[SYNC] Syncing ${batchSize} blocks (${this.syncState.localHeight + 1} → ${this.syncState.localHeight + batchSize})`);
|
|
1396
|
+
}
|
|
1332
1397
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1398
|
+
for (let i = 1; i <= batchSize; i++) {
|
|
1399
|
+
await fetchBlock(this.syncState.localHeight + i);
|
|
1400
|
+
}
|
|
1336
1401
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
1347
|
-
|
|
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
|
-
//
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
-
//
|
|
1365
|
-
|
|
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.
|
|
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
|
-
"
|
|
66
|
-
"
|
|
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/",
|