jaelis-node 1.2.0 → 1.3.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.
@@ -76,6 +76,7 @@ program
76
76
  .option('--rpc-host <host>', 'RPC server host', '0.0.0.0')
77
77
  .option('--no-rpc', 'Disable RPC server')
78
78
  .option('--sync-mode <mode>', 'Sync mode: full, light, archive', 'full')
79
+ .option('--reward-recipient <address>', 'Wallet address to receive node rewards (ANY chain format!)')
79
80
  .action(async (options) => {
80
81
  console.log(chalk.cyan(BANNER));
81
82
  console.log(chalk.green('Starting JAELIS Node...'));
@@ -94,6 +95,9 @@ program
94
95
  console.log(chalk.gray(` P2P Port: ${options.p2pPort}`));
95
96
  console.log(chalk.gray(` Data Dir: ${options.dataDir}`));
96
97
  console.log(chalk.gray(` Sync Mode: ${options.syncMode}`));
98
+ if (options.rewardRecipient) {
99
+ console.log(chalk.gray(` Rewards To: ${options.rewardRecipient}`));
100
+ }
97
101
  console.log();
98
102
 
99
103
  const spinner = ora('Initializing node...').start();
@@ -111,7 +115,8 @@ program
111
115
  dataDir: options.dataDir,
112
116
  syncMode: options.syncMode,
113
117
  enableRpc: options.rpc,
114
- bootstrapNodes: network.bootstrapNodes
118
+ bootstrapNodes: network.bootstrapNodes,
119
+ rewardRecipient: options.rewardRecipient // Like Geth's --suggested-fee-recipient
115
120
  });
116
121
 
117
122
  spinner.text = 'Connecting to network...';
@@ -311,5 +316,201 @@ program
311
316
  console.log(chalk.cyan('Website: https://jaelis.io'));
312
317
  });
313
318
 
319
+ // ═══════════════════════════════════════════════════════════════════════════
320
+ // WALLET COMMANDS - Manage node wallet configuration
321
+ // Like Geth's account management, but supports ANY chain address!
322
+ // NO STAKING REQUIRED - JAELIS doesn't hold your funds!
323
+ // ═══════════════════════════════════════════════════════════════════════════
324
+
325
+ // WALLET SET-RECIPIENT - Set reward recipient address (like Geth --suggested-fee-recipient)
326
+ program
327
+ .command('wallet:set-recipient <address>')
328
+ .description('Set the wallet address to receive node rewards (accepts ANY chain format!)')
329
+ .option('-d, --data-dir <path>', 'Data directory', './jaelis-data')
330
+ .action((address, options) => {
331
+ console.log(chalk.cyan('Setting reward recipient...'));
332
+ console.log();
333
+
334
+ try {
335
+ const { NodeWalletConfig, validateAddress } = require('../lib/index.js');
336
+
337
+ // Validate address format
338
+ const validation = validateAddress(address);
339
+ if (!validation.valid) {
340
+ console.log(chalk.red('✗ Invalid address format'));
341
+ console.log(chalk.gray(' Supported formats:'));
342
+ console.log(chalk.gray(' • EVM (Ethereum, Polygon, etc.): 0x...'));
343
+ console.log(chalk.gray(' • Solana: base58 address'));
344
+ console.log(chalk.gray(' • Bitcoin: 1..., 3..., or bc1...'));
345
+ console.log(chalk.gray(' • TON: base64url address'));
346
+ console.log(chalk.gray(' • Move (Aptos/Sui): 0x... (64 chars)'));
347
+ process.exit(1);
348
+ }
349
+
350
+ const walletConfig = new NodeWalletConfig(options.dataDir);
351
+ walletConfig.load();
352
+ const result = walletConfig.setRewardRecipient(address);
353
+
354
+ console.log(chalk.green('✓ Reward recipient set successfully!'));
355
+ console.log();
356
+ console.log(chalk.white(' Address: ') + chalk.cyan(result.originalFormat));
357
+ console.log(chalk.white(' Type: ') + chalk.gray(result.typeName));
358
+ console.log();
359
+ console.log(chalk.gray(' Node rewards will be sent to this address.'));
360
+ console.log(chalk.gray(' NO STAKING REQUIRED - you keep your own funds!'));
361
+
362
+ } catch (error) {
363
+ console.log(chalk.red('✗ Failed to set recipient'));
364
+ console.log(chalk.gray(error.message));
365
+ process.exit(1);
366
+ }
367
+ });
368
+
369
+ // WALLET ADD - Add an additional wallet
370
+ program
371
+ .command('wallet:add <name> <address>')
372
+ .description('Add a wallet to this node (accepts ANY chain format!)')
373
+ .option('-d, --data-dir <path>', 'Data directory', './jaelis-data')
374
+ .option('-p, --purpose <purpose>', 'Wallet purpose (general, validator, development)', 'general')
375
+ .action((name, address, options) => {
376
+ console.log(chalk.cyan('Adding wallet...'));
377
+ console.log();
378
+
379
+ try {
380
+ const { NodeWalletConfig, validateAddress } = require('../lib/index.js');
381
+
382
+ const validation = validateAddress(address);
383
+ if (!validation.valid) {
384
+ console.log(chalk.red('✗ Invalid address format'));
385
+ process.exit(1);
386
+ }
387
+
388
+ const walletConfig = new NodeWalletConfig(options.dataDir);
389
+ walletConfig.load();
390
+ const result = walletConfig.addWallet(name, address, { purpose: options.purpose });
391
+
392
+ console.log(chalk.green('✓ Wallet added successfully!'));
393
+ console.log();
394
+ console.log(chalk.white(' Name: ') + chalk.cyan(result.name));
395
+ console.log(chalk.white(' Address: ') + chalk.gray(result.originalFormat));
396
+ console.log(chalk.white(' Type: ') + chalk.gray(result.typeName));
397
+ console.log(chalk.white(' Purpose: ') + chalk.gray(result.purpose));
398
+
399
+ } catch (error) {
400
+ console.log(chalk.red('✗ Failed to add wallet'));
401
+ console.log(chalk.gray(error.message));
402
+ process.exit(1);
403
+ }
404
+ });
405
+
406
+ // WALLET REMOVE - Remove a wallet
407
+ program
408
+ .command('wallet:remove <name-or-address>')
409
+ .description('Remove a wallet from this node')
410
+ .option('-d, --data-dir <path>', 'Data directory', './jaelis-data')
411
+ .action((nameOrAddress, options) => {
412
+ console.log(chalk.cyan('Removing wallet...'));
413
+ console.log();
414
+
415
+ try {
416
+ const { NodeWalletConfig } = require('../lib/index.js');
417
+
418
+ const walletConfig = new NodeWalletConfig(options.dataDir);
419
+ walletConfig.load();
420
+ const result = walletConfig.removeWallet(nameOrAddress);
421
+
422
+ console.log(chalk.green('✓ Wallet removed!'));
423
+ console.log(chalk.gray(` Removed: ${result.name} (${result.originalFormat})`));
424
+
425
+ } catch (error) {
426
+ console.log(chalk.red('✗ Failed to remove wallet'));
427
+ console.log(chalk.gray(error.message));
428
+ process.exit(1);
429
+ }
430
+ });
431
+
432
+ // WALLET LIST - List all wallets
433
+ program
434
+ .command('wallet:list')
435
+ .description('List all configured wallets')
436
+ .option('-d, --data-dir <path>', 'Data directory', './jaelis-data')
437
+ .action((options) => {
438
+ console.log(chalk.cyan('Node Wallet Configuration'));
439
+ console.log();
440
+
441
+ try {
442
+ const { NodeWalletConfig } = require('../lib/index.js');
443
+
444
+ const walletConfig = new NodeWalletConfig(options.dataDir);
445
+ const config = walletConfig.listWallets();
446
+
447
+ // Show reward recipient
448
+ if (config.rewardRecipient) {
449
+ console.log(chalk.white('Reward Recipient:'));
450
+ console.log(chalk.green(` ★ ${config.rewardRecipient.originalFormat}`));
451
+ console.log(chalk.gray(` Type: ${config.rewardRecipient.typeName}`));
452
+ console.log();
453
+ } else {
454
+ console.log(chalk.yellow('⚠ No reward recipient set!'));
455
+ console.log(chalk.gray(' Run: jaelis-node wallet:set-recipient <your-address>'));
456
+ console.log();
457
+ }
458
+
459
+ // Show additional wallets
460
+ if (config.wallets && config.wallets.length > 0) {
461
+ console.log(chalk.white('Additional Wallets:'));
462
+ config.wallets.forEach((wallet, i) => {
463
+ console.log(chalk.cyan(` ${i + 1}. ${wallet.name}`));
464
+ console.log(chalk.gray(` Address: ${wallet.originalFormat}`));
465
+ console.log(chalk.gray(` Type: ${wallet.typeName}`));
466
+ console.log(chalk.gray(` Purpose: ${wallet.purpose}`));
467
+ });
468
+ } else {
469
+ console.log(chalk.gray('No additional wallets configured.'));
470
+ }
471
+
472
+ console.log();
473
+ console.log(chalk.gray('Supported address formats:'));
474
+ console.log(chalk.gray(' • EVM (Ethereum, Polygon, Arbitrum, Base, etc.)'));
475
+ console.log(chalk.gray(' • Solana (base58)'));
476
+ console.log(chalk.gray(' • Bitcoin (Legacy & SegWit)'));
477
+ console.log(chalk.gray(' • TON'));
478
+ console.log(chalk.gray(' • Move (Aptos/Sui)'));
479
+
480
+ } catch (error) {
481
+ console.log(chalk.red('Failed to list wallets'));
482
+ console.log(chalk.gray(error.message));
483
+ }
484
+ });
485
+
486
+ // WALLET IDENTITY - Show or generate node identity
487
+ program
488
+ .command('wallet:identity')
489
+ .description('Show or generate the node identity (like Solana identity keypair)')
490
+ .option('-d, --data-dir <path>', 'Data directory', './jaelis-data')
491
+ .action((options) => {
492
+ console.log(chalk.cyan('Node Identity'));
493
+ console.log();
494
+
495
+ try {
496
+ const { NodeWalletConfig } = require('../lib/index.js');
497
+
498
+ const walletConfig = new NodeWalletConfig(options.dataDir);
499
+ walletConfig.load();
500
+ const identity = walletConfig.getNodeIdentity();
501
+
502
+ console.log(chalk.white('Node Identity:'));
503
+ console.log(chalk.green(` Public Key: ${identity.publicKey}`));
504
+ console.log(chalk.gray(` Created: ${new Date(identity.createdAt).toISOString()}`));
505
+ console.log();
506
+ console.log(chalk.gray('This identity is used to identify your node on the network.'));
507
+ console.log(chalk.gray('It is NOT a wallet - use wallet:set-recipient for rewards.'));
508
+
509
+ } catch (error) {
510
+ console.log(chalk.red('Failed to get node identity'));
511
+ console.log(chalk.gray(error.message));
512
+ }
513
+ });
514
+
314
515
  // Parse and execute
315
516
  program.parse();
package/lib/index.js CHANGED
@@ -11,8 +11,310 @@
11
11
 
12
12
  const path = require('path');
13
13
  const fs = require('fs');
14
+ const crypto = require('crypto');
14
15
  const EventEmitter = require('events');
15
16
 
17
+ // ═══════════════════════════════════════════════════════════════════════════
18
+ // MULTI-CHAIN ADDRESS UTILITIES
19
+ // Supports: EVM (0x...), Solana (base58), Bitcoin (P2PKH/bech32), etc.
20
+ // JAELIS accepts ANY chain's address format - TRUE multi-chain!
21
+ // ═══════════════════════════════════════════════════════════════════════════
22
+
23
+ const ADDRESS_FORMATS = {
24
+ // EVM chains (Ethereum, Polygon, Arbitrum, etc.)
25
+ evm: {
26
+ pattern: /^0x[a-fA-F0-9]{40}$/,
27
+ name: 'EVM (Ethereum, Polygon, etc.)',
28
+ example: '0x742d35Cc6634C0532925a3b844Bc9e7595f...'
29
+ },
30
+ // Solana (base58)
31
+ solana: {
32
+ pattern: /^[1-9A-HJ-NP-Za-km-z]{32,44}$/,
33
+ name: 'Solana',
34
+ example: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZ...'
35
+ },
36
+ // Bitcoin P2PKH (starts with 1 or 3)
37
+ bitcoin_legacy: {
38
+ pattern: /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/,
39
+ name: 'Bitcoin (Legacy)',
40
+ example: '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2'
41
+ },
42
+ // Bitcoin bech32 (starts with bc1)
43
+ bitcoin_bech32: {
44
+ pattern: /^bc1[a-z0-9]{39,59}$/,
45
+ name: 'Bitcoin (SegWit)',
46
+ example: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'
47
+ },
48
+ // TON (base64url)
49
+ ton: {
50
+ pattern: /^[A-Za-z0-9_-]{48}$/,
51
+ name: 'TON',
52
+ example: 'EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N'
53
+ },
54
+ // Aptos/Sui (hex with 0x, 64 chars)
55
+ move: {
56
+ pattern: /^0x[a-fA-F0-9]{64}$/,
57
+ name: 'Move (Aptos/Sui)',
58
+ example: '0x1234567890abcdef...'
59
+ },
60
+ // JAELIS native (same as EVM for now, but could be extended)
61
+ jaelis: {
62
+ pattern: /^0x[a-fA-F0-9]{40}$/,
63
+ name: 'JAELIS Native',
64
+ example: '0x742d35Cc6634C0532925a3b844Bc9e7595f...'
65
+ }
66
+ };
67
+
68
+ /**
69
+ * Detect the chain type from an address
70
+ */
71
+ function detectAddressType(address) {
72
+ if (!address || typeof address !== 'string') {
73
+ return null;
74
+ }
75
+
76
+ // Check each format
77
+ for (const [type, config] of Object.entries(ADDRESS_FORMATS)) {
78
+ if (config.pattern.test(address)) {
79
+ return { type, ...config };
80
+ }
81
+ }
82
+
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * Validate any chain address
88
+ */
89
+ function validateAddress(address) {
90
+ const detected = detectAddressType(address);
91
+ if (!detected) {
92
+ return {
93
+ valid: false,
94
+ error: 'Unknown address format',
95
+ supportedFormats: Object.keys(ADDRESS_FORMATS)
96
+ };
97
+ }
98
+ return {
99
+ valid: true,
100
+ type: detected.type,
101
+ name: detected.name
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Convert any address to canonical form for internal storage
107
+ * EVM addresses are lowercased, others kept as-is
108
+ */
109
+ function toCanonicalAddress(address) {
110
+ const detected = detectAddressType(address);
111
+ if (!detected) {
112
+ throw new Error('Invalid address format');
113
+ }
114
+
115
+ // EVM addresses are case-insensitive, normalize to lowercase
116
+ if (detected.type === 'evm' || detected.type === 'jaelis') {
117
+ return address.toLowerCase();
118
+ }
119
+
120
+ // Other formats are case-sensitive
121
+ return address;
122
+ }
123
+
124
+ // ═══════════════════════════════════════════════════════════════════════════
125
+ // NODE WALLET CONFIGURATION (Like Geth --suggested-fee-recipient)
126
+ // Stores the node operator's wallet for receiving rewards
127
+ // NO STAKING REQUIRED - JAELIS doesn't hold your funds!
128
+ // ═══════════════════════════════════════════════════════════════════════════
129
+
130
+ class NodeWalletConfig {
131
+ constructor(dataDir) {
132
+ this.dataDir = dataDir;
133
+ this.configPath = path.join(dataDir, 'node-wallet.json');
134
+ this.config = null;
135
+ }
136
+
137
+ /**
138
+ * Load wallet configuration from disk
139
+ */
140
+ load() {
141
+ try {
142
+ if (fs.existsSync(this.configPath)) {
143
+ const data = fs.readFileSync(this.configPath, 'utf8');
144
+ this.config = JSON.parse(data);
145
+ return this.config;
146
+ }
147
+ } catch (e) {
148
+ console.warn('[WALLET] Failed to load wallet config:', e.message);
149
+ }
150
+ return null;
151
+ }
152
+
153
+ /**
154
+ * Save wallet configuration to disk
155
+ */
156
+ save() {
157
+ try {
158
+ // Ensure directory exists
159
+ if (!fs.existsSync(this.dataDir)) {
160
+ fs.mkdirSync(this.dataDir, { recursive: true });
161
+ }
162
+ fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
163
+ return true;
164
+ } catch (e) {
165
+ console.error('[WALLET] Failed to save wallet config:', e.message);
166
+ return false;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Set the reward recipient wallet (ANY chain address!)
172
+ * Like Geth's --suggested-fee-recipient
173
+ */
174
+ setRewardRecipient(address) {
175
+ const validation = validateAddress(address);
176
+ if (!validation.valid) {
177
+ throw new Error(`Invalid address: ${validation.error}`);
178
+ }
179
+
180
+ if (!this.config) {
181
+ this.config = { createdAt: Date.now() };
182
+ }
183
+
184
+ this.config.rewardRecipient = {
185
+ address: toCanonicalAddress(address),
186
+ originalFormat: address,
187
+ type: validation.type,
188
+ typeName: validation.name,
189
+ setAt: Date.now()
190
+ };
191
+
192
+ this.save();
193
+ console.log(`[WALLET] Reward recipient set: ${address} (${validation.name})`);
194
+ return this.config.rewardRecipient;
195
+ }
196
+
197
+ /**
198
+ * Get the reward recipient address
199
+ */
200
+ getRewardRecipient() {
201
+ if (!this.config) this.load();
202
+ return this.config?.rewardRecipient || null;
203
+ }
204
+
205
+ /**
206
+ * Add an additional wallet (for multi-wallet nodes)
207
+ */
208
+ addWallet(name, address, options = {}) {
209
+ const validation = validateAddress(address);
210
+ if (!validation.valid) {
211
+ throw new Error(`Invalid address: ${validation.error}`);
212
+ }
213
+
214
+ if (!this.config) {
215
+ this.config = { createdAt: Date.now() };
216
+ }
217
+
218
+ if (!this.config.wallets) {
219
+ this.config.wallets = [];
220
+ }
221
+
222
+ // Check for duplicates
223
+ const existing = this.config.wallets.find(w =>
224
+ toCanonicalAddress(w.address) === toCanonicalAddress(address)
225
+ );
226
+ if (existing) {
227
+ throw new Error(`Wallet already exists: ${existing.name}`);
228
+ }
229
+
230
+ const wallet = {
231
+ name,
232
+ address: toCanonicalAddress(address),
233
+ originalFormat: address,
234
+ type: validation.type,
235
+ typeName: validation.name,
236
+ purpose: options.purpose || 'general',
237
+ addedAt: Date.now()
238
+ };
239
+
240
+ this.config.wallets.push(wallet);
241
+ this.save();
242
+ console.log(`[WALLET] Added wallet: ${name} (${validation.name})`);
243
+ return wallet;
244
+ }
245
+
246
+ /**
247
+ * Remove a wallet by name or address
248
+ */
249
+ removeWallet(nameOrAddress) {
250
+ if (!this.config?.wallets) {
251
+ throw new Error('No wallets configured');
252
+ }
253
+
254
+ const canonical = nameOrAddress.startsWith('0x') || nameOrAddress.startsWith('bc1')
255
+ ? toCanonicalAddress(nameOrAddress)
256
+ : null;
257
+
258
+ const index = this.config.wallets.findIndex(w =>
259
+ w.name === nameOrAddress ||
260
+ (canonical && toCanonicalAddress(w.address) === canonical)
261
+ );
262
+
263
+ if (index === -1) {
264
+ throw new Error(`Wallet not found: ${nameOrAddress}`);
265
+ }
266
+
267
+ const removed = this.config.wallets.splice(index, 1)[0];
268
+ this.save();
269
+ console.log(`[WALLET] Removed wallet: ${removed.name}`);
270
+ return removed;
271
+ }
272
+
273
+ /**
274
+ * List all configured wallets
275
+ */
276
+ listWallets() {
277
+ if (!this.config) this.load();
278
+ return {
279
+ rewardRecipient: this.config?.rewardRecipient || null,
280
+ wallets: this.config?.wallets || []
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Get node identity (generates one if not exists)
286
+ * Like Solana's identity keypair
287
+ */
288
+ getNodeIdentity() {
289
+ if (!this.config) this.load();
290
+ if (!this.config) {
291
+ this.config = { createdAt: Date.now() };
292
+ }
293
+
294
+ if (!this.config.nodeIdentity) {
295
+ // Generate a node identity (not a wallet, just an ID)
296
+ const nodeId = crypto.randomBytes(32).toString('hex');
297
+ this.config.nodeIdentity = {
298
+ id: nodeId,
299
+ publicKey: '0x' + crypto.createHash('sha256').update(nodeId).digest('hex').slice(0, 40),
300
+ createdAt: Date.now()
301
+ };
302
+ this.save();
303
+ console.log(`[WALLET] Generated node identity: ${this.config.nodeIdentity.publicKey}`);
304
+ }
305
+
306
+ return this.config.nodeIdentity;
307
+ }
308
+
309
+ /**
310
+ * Get full configuration
311
+ */
312
+ getConfig() {
313
+ if (!this.config) this.load();
314
+ return this.config;
315
+ }
316
+ }
317
+
16
318
  // JAELIS Network Endpoints - users connect here automatically
17
319
  const JAELIS_NETWORKS = {
18
320
  testnet: {
@@ -73,6 +375,65 @@ class JaelisNode extends EventEmitter {
73
375
  this.isRunning = false;
74
376
  this.startTime = null;
75
377
  this.peerCount = 0;
378
+
379
+ // Initialize wallet configuration (like Geth's keystore)
380
+ this.walletConfig = new NodeWalletConfig(this.options.dataDir);
381
+ this.walletConfig.load();
382
+
383
+ // Set reward recipient if provided via options (like --suggested-fee-recipient)
384
+ if (options.rewardRecipient) {
385
+ this.walletConfig.setRewardRecipient(options.rewardRecipient);
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Get the wallet configuration manager
391
+ */
392
+ getWalletConfig() {
393
+ return this.walletConfig;
394
+ }
395
+
396
+ /**
397
+ * Set the reward recipient address (ANY chain format!)
398
+ * Like Geth's --suggested-fee-recipient
399
+ */
400
+ setRewardRecipient(address) {
401
+ return this.walletConfig.setRewardRecipient(address);
402
+ }
403
+
404
+ /**
405
+ * Get the reward recipient address
406
+ */
407
+ getRewardRecipient() {
408
+ return this.walletConfig.getRewardRecipient();
409
+ }
410
+
411
+ /**
412
+ * Add a wallet to this node
413
+ */
414
+ addWallet(name, address, options = {}) {
415
+ return this.walletConfig.addWallet(name, address, options);
416
+ }
417
+
418
+ /**
419
+ * Remove a wallet from this node
420
+ */
421
+ removeWallet(nameOrAddress) {
422
+ return this.walletConfig.removeWallet(nameOrAddress);
423
+ }
424
+
425
+ /**
426
+ * List all configured wallets
427
+ */
428
+ listWallets() {
429
+ return this.walletConfig.listWallets();
430
+ }
431
+
432
+ /**
433
+ * Get node identity
434
+ */
435
+ getNodeIdentity() {
436
+ return this.walletConfig.getNodeIdentity();
76
437
  }
77
438
 
78
439
  /**
@@ -110,6 +471,12 @@ class JaelisNode extends EventEmitter {
110
471
  this.isRunning = true;
111
472
  this.startTime = Date.now();
112
473
 
474
+ // Register with the JAELIS network for tracking
475
+ await this._registerWithNetwork();
476
+
477
+ // Start heartbeat to keep registration alive
478
+ this._startHeartbeat();
479
+
113
480
  this.emit('started', {
114
481
  network: this.options.network,
115
482
  chainId: this.options.chainId,
@@ -135,6 +502,9 @@ class JaelisNode extends EventEmitter {
135
502
 
136
503
  console.log('[JAELIS] Stopping node...');
137
504
 
505
+ // Stop heartbeat first
506
+ this._stopHeartbeat();
507
+
138
508
  // Stop RPC server
139
509
  if (this.rpcServer) {
140
510
  await this._stopRpcServer();
@@ -275,6 +645,141 @@ class JaelisNode extends EventEmitter {
275
645
  }
276
646
  }
277
647
 
648
+ /**
649
+ * Register this node with the JAELIS network for tracking
650
+ * Mario can see all nodes that connect! Lifetime tracking!
651
+ */
652
+ async _registerWithNetwork() {
653
+ try {
654
+ const https = require('https');
655
+ const http = require('http');
656
+
657
+ const identity = this.walletConfig.getNodeIdentity();
658
+ const rewardRecipient = this.walletConfig.getRewardRecipient();
659
+
660
+ const nodeInfo = {
661
+ nodeId: identity.id,
662
+ publicKey: identity.publicKey,
663
+ network: this.options.network,
664
+ chainId: this.options.chainId,
665
+ version: '1.3.0',
666
+ rewardAddress: rewardRecipient?.address || null,
667
+ rewardChainType: rewardRecipient?.type || null,
668
+ capabilities: ['full-node', 'rpc'],
669
+ p2pPort: this.options.p2pPort,
670
+ rpcPort: this.options.rpcPort
671
+ };
672
+
673
+ // Store for heartbeat
674
+ this._nodeInfo = nodeInfo;
675
+
676
+ const data = JSON.stringify({
677
+ jsonrpc: '2.0',
678
+ method: 'jaelis_node_register',
679
+ params: [nodeInfo],
680
+ id: 1
681
+ });
682
+
683
+ const url = new URL(this.options.remoteRpc);
684
+ const protocol = url.protocol === 'https:' ? https : http;
685
+
686
+ const options = {
687
+ hostname: url.hostname,
688
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
689
+ path: url.pathname || '/',
690
+ method: 'POST',
691
+ headers: {
692
+ 'Content-Type': 'application/json',
693
+ 'Content-Length': Buffer.byteLength(data)
694
+ }
695
+ };
696
+
697
+ await new Promise((resolve) => {
698
+ const req = protocol.request(options, (res) => {
699
+ let body = '';
700
+ res.on('data', chunk => body += chunk);
701
+ res.on('end', () => {
702
+ try {
703
+ const result = JSON.parse(body);
704
+ if (result.result?.registered) {
705
+ console.log(`[JAELIS] Node registered with network (ID: ${identity.publicKey.slice(0, 12)}...)`);
706
+ }
707
+ } catch (e) {
708
+ // Silent - registration is best-effort
709
+ }
710
+ resolve();
711
+ });
712
+ });
713
+
714
+ req.on('error', () => resolve()); // Silent fail - will retry on heartbeat
715
+ req.write(data);
716
+ req.end();
717
+ });
718
+
719
+ } catch (error) {
720
+ // Registration is best-effort, don't fail node startup
721
+ console.log('[JAELIS] Network registration pending (will retry)');
722
+ }
723
+ }
724
+
725
+ /**
726
+ * Start heartbeat to keep node registration alive
727
+ * Sends heartbeat every 60 seconds
728
+ */
729
+ _startHeartbeat() {
730
+ const HEARTBEAT_INTERVAL = 60000; // 60 seconds
731
+
732
+ this._heartbeatInterval = setInterval(async () => {
733
+ if (!this.isRunning || !this._nodeInfo) return;
734
+
735
+ try {
736
+ const https = require('https');
737
+ const http = require('http');
738
+
739
+ const data = JSON.stringify({
740
+ jsonrpc: '2.0',
741
+ method: 'jaelis_node_heartbeat',
742
+ params: [this._nodeInfo.nodeId],
743
+ id: 1
744
+ });
745
+
746
+ const url = new URL(this.options.remoteRpc);
747
+ const protocol = url.protocol === 'https:' ? https : http;
748
+
749
+ const options = {
750
+ hostname: url.hostname,
751
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
752
+ path: url.pathname || '/',
753
+ method: 'POST',
754
+ headers: {
755
+ 'Content-Type': 'application/json',
756
+ 'Content-Length': Buffer.byteLength(data)
757
+ }
758
+ };
759
+
760
+ const req = protocol.request(options, () => {});
761
+ req.on('error', () => {}); // Silent
762
+ req.write(data);
763
+ req.end();
764
+
765
+ } catch (e) {
766
+ // Silent - heartbeat is best-effort
767
+ }
768
+ }, HEARTBEAT_INTERVAL);
769
+
770
+ console.log('[JAELIS] Heartbeat started (60s interval)');
771
+ }
772
+
773
+ /**
774
+ * Stop heartbeat
775
+ */
776
+ _stopHeartbeat() {
777
+ if (this._heartbeatInterval) {
778
+ clearInterval(this._heartbeatInterval);
779
+ this._heartbeatInterval = null;
780
+ }
781
+ }
782
+
278
783
  /**
279
784
  * Get node status
280
785
  */
@@ -1020,6 +1525,15 @@ module.exports = {
1020
1525
  EmbeddedRpcServer,
1021
1526
  EmbeddedStorage,
1022
1527
 
1528
+ // Wallet configuration (like Geth's keystore)
1529
+ NodeWalletConfig,
1530
+
1531
+ // Multi-chain address utilities
1532
+ ADDRESS_FORMATS,
1533
+ detectAddressType,
1534
+ validateAddress,
1535
+ toCanonicalAddress,
1536
+
1023
1537
  // Convenience factory
1024
1538
  createNode: (options) => new JaelisNode(options),
1025
1539
  createBootstrap: (options) => new BootstrapNode(options)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jaelis-node",
3
- "version": "1.2.0",
4
- "description": "Official JAELIS Blockchain Node - Run a full node, validator, or bootstrap node with Cross-Chain Settlement (30+ chains!)",
3
+ "version": "1.3.0",
4
+ "description": "Official JAELIS Blockchain Node - Run a full node, validator, or bootstrap node with Cross-Chain Settlement (30+ chains!), ERC-5792 wallet capabilities, EIP-7702 account abstraction",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
7
7
  "jaelis-node": "./bin/jaelis-node.js"