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.
- package/bin/jaelis-node.js +202 -1
- package/lib/index.js +514 -0
- package/package.json +2 -2
package/bin/jaelis-node.js
CHANGED
|
@@ -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.
|
|
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"
|