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 +67 -1
- package/bin/jaelis-node.js +16 -3
- package/lib/index.js +325 -50
- package/package.json +4 -3
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:
|
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',
|
|
45
|
-
//
|
|
46
|
-
//
|
|
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/
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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':
|
|
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':
|
|
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:
|
|
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
|
-
//
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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.
|
|
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
|
-
//
|
|
1346
|
-
const
|
|
1347
|
-
|
|
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
|
-
//
|
|
1359
|
-
|
|
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
|
-
|
|
1362
|
-
|
|
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 <=
|
|
1365
|
-
|
|
1366
|
-
|
|
1498
|
+
for (let i = 1; i <= batchSize; i++) {
|
|
1499
|
+
await fetchBlock(this.syncState.localHeight + i);
|
|
1500
|
+
}
|
|
1367
1501
|
|
|
1368
|
-
|
|
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
|
-
|
|
1373
|
-
|
|
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
|
-
|
|
1382
|
-
|
|
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
|
-
//
|
|
1396
|
-
|
|
1397
|
-
|
|
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
|
-
|
|
1400
|
-
|
|
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.
|
|
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/",
|