@pioneer-platform/eth-network 8.13.16 → 8.14.3
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/.turbo/turbo-build.log +1 -2
- package/CHANGELOG.md +29 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +287 -141
- package/package.json +2 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
[0m[2m[35m$[0m [2m[1mtsc -p .[0m
|
|
1
|
+
$ tsc -p .
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @pioneer-platform/eth-network
|
|
2
2
|
|
|
3
|
+
## 8.14.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fix: Add node racing to all eth-network \*ByNetwork functions
|
|
8
|
+
|
|
9
|
+
## 8.14.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- fix: Add node racing to all eth-network \*ByNetwork functions
|
|
14
|
+
|
|
15
|
+
## 8.14.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- cache work
|
|
20
|
+
|
|
21
|
+
## 8.14.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- Merge branch 'feature/ethereum-reports'
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- Updated dependencies
|
|
30
|
+
- @pioneer-platform/blockbook@8.12.0
|
|
31
|
+
|
|
3
32
|
## 8.13.16
|
|
4
33
|
|
|
5
34
|
### Patch Changes
|
package/lib/index.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export declare const addNode: (node: any) => number;
|
|
|
18
18
|
export declare const addNodes: (nodeList: any) => void;
|
|
19
19
|
/**
|
|
20
20
|
* Get nonce for address
|
|
21
|
+
* @deprecated Use getNonceByNetwork instead - this method relies on environment variables
|
|
22
|
+
* and does not use the node racing/fallback system
|
|
21
23
|
*/
|
|
22
24
|
export declare const getNonce: (address: string) => Promise<any>;
|
|
23
25
|
/**
|
|
@@ -26,6 +28,8 @@ export declare const getNonce: (address: string) => Promise<any>;
|
|
|
26
28
|
export declare const getNonceByNetwork: (networkId: string, address: string) => Promise<number>;
|
|
27
29
|
/**
|
|
28
30
|
* Get current gas price
|
|
31
|
+
* @deprecated Use getGasPriceByNetwork instead - this method relies on environment variables
|
|
32
|
+
* and does not use the node racing/fallback system
|
|
29
33
|
*/
|
|
30
34
|
export declare const getGasPrice: () => Promise<any>;
|
|
31
35
|
/**
|
|
@@ -38,10 +42,13 @@ export declare const getGasPriceByNetwork: (networkId: string) => Promise<import
|
|
|
38
42
|
export declare const estimateFee: (asset: any, params: any) => Promise<any>;
|
|
39
43
|
/**
|
|
40
44
|
* Get ETH balance for address
|
|
45
|
+
* @deprecated Use getBalanceAddressByNetwork instead - this method relies on environment variables
|
|
46
|
+
* and does not use the node racing/fallback system with health tracking
|
|
41
47
|
*/
|
|
42
48
|
export declare const getBalance: (address: string) => Promise<string>;
|
|
43
49
|
/**
|
|
44
50
|
* Get balances for multiple addresses
|
|
51
|
+
* @deprecated Use getBalanceAddressByNetwork in a loop instead - this method relies on environment variables
|
|
45
52
|
*/
|
|
46
53
|
export declare const getBalances: (addresses: string[]) => Promise<string[]>;
|
|
47
54
|
/**
|
package/lib/index.js
CHANGED
|
@@ -64,7 +64,8 @@ const NodeHealth = __importStar(require("./node-health"));
|
|
|
64
64
|
const log = require('@pioneer-platform/loggerdog')();
|
|
65
65
|
let wait = require('wait-promise');
|
|
66
66
|
let sleep = wait.sleep;
|
|
67
|
-
//
|
|
67
|
+
// Legacy provider instances - DEPRECATED, kept for backward compatibility only
|
|
68
|
+
// All production methods should use *ByNetwork variants with node racing
|
|
68
69
|
let ETHERSCAN;
|
|
69
70
|
let PROVIDER;
|
|
70
71
|
let PROVIDER_BASE;
|
|
@@ -112,14 +113,14 @@ const TIER_PRIORITY = {
|
|
|
112
113
|
// - VALIDATE_NODES_ON_STARTUP: Set to 'false' to disable background validation (default: enabled)
|
|
113
114
|
// - STARTUP_NODE_TIMEOUT: Timeout per node in ms (default: 3000)
|
|
114
115
|
// - STARTUP_NODE_CONCURRENCY: Number of parallel validation workers (default: 20)
|
|
115
|
-
// - STARTUP_VALIDATION_TIER: Comma-separated tiers to validate on startup (default: 'premium,reliable')
|
|
116
|
+
// - STARTUP_VALIDATION_TIER: Comma-separated tiers to validate on startup (default: 'premium,reliable,public')
|
|
116
117
|
// - STARTUP_NODE_SAMPLE: Number of random nodes to test per tier (default: 0 = all)
|
|
117
118
|
//
|
|
118
119
|
// Note: Validation runs in background and does NOT block server startup
|
|
119
120
|
const STARTUP_VALIDATION_ENABLED = process.env.VALIDATE_NODES_ON_STARTUP !== 'false'; // Default true
|
|
120
121
|
const STARTUP_VALIDATION_TIMEOUT = parseInt(process.env.STARTUP_NODE_TIMEOUT || '3000'); // 3s timeout
|
|
121
122
|
const STARTUP_VALIDATION_CONCURRENCY = parseInt(process.env.STARTUP_NODE_CONCURRENCY || '20'); // 20 parallel
|
|
122
|
-
const STARTUP_VALIDATION_TIERS = (process.env.STARTUP_VALIDATION_TIER || 'premium,reliable').split(',');
|
|
123
|
+
const STARTUP_VALIDATION_TIERS = (process.env.STARTUP_VALIDATION_TIER || 'premium,reliable,public').split(',');
|
|
123
124
|
const STARTUP_VALIDATION_SAMPLE_SIZE = parseInt(process.env.STARTUP_NODE_SAMPLE || '0'); // 0 = all nodes in tier
|
|
124
125
|
/**
|
|
125
126
|
* Check if a node is marked as dead
|
|
@@ -291,26 +292,22 @@ const init = async function (settings, redis) {
|
|
|
291
292
|
log.error(tag, 'Background node validation failed:', err);
|
|
292
293
|
});
|
|
293
294
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
// DEPRECATED: Legacy provider initialization for backward compatibility
|
|
296
|
+
// Modern code should use *ByNetwork methods which leverage the full node racing system
|
|
297
|
+
// These globals are only initialized for legacy methods (getNonce, getBalance, etc.)
|
|
298
|
+
// and are NOT used by production endpoints in evm.controller.ts
|
|
299
|
+
// Only initialize Etherscan for legacy compatibility
|
|
300
|
+
if (process.env['ETHERSCAN_API_KEY']) {
|
|
298
301
|
ETHERSCAN = new providers_1.EtherscanProvider('mainnet', process.env['ETHERSCAN_API_KEY']);
|
|
299
|
-
NODE_URL = process.env['PARITY_ARCHIVE_NODE'] || 'https://eth.llamarpc.com';
|
|
300
|
-
}
|
|
301
|
-
else if (settings.testnet) {
|
|
302
|
-
if (!process.env['INFURA_TESTNET_ROPSTEN'])
|
|
303
|
-
throw Error("Missing INFURA_TESTNET_ROPSTEN");
|
|
304
|
-
if (!process.env['ETHERSCAN_API_KEY'])
|
|
305
|
-
throw Error("Missing ETHERSCAN_API_KEY");
|
|
306
|
-
PROVIDER = new ethers.providers.JsonRpcProvider(process.env['INFURA_TESTNET_ROPSTEN']);
|
|
307
|
-
NODE_URL = process.env['INFURA_TESTNET_ROPSTEN'];
|
|
308
|
-
ETHERSCAN = new providers_1.EtherscanProvider('ropsten', process.env['ETHERSCAN_API_KEY']);
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
PROVIDER = new ethers.providers.JsonRpcProvider(process.env['PARITY_ARCHIVE_NODE'] || 'https://eth.llamarpc.com');
|
|
312
302
|
}
|
|
303
|
+
// REMOVED: PROVIDER, PROVIDER_BASE, NODE_URL initialization
|
|
304
|
+
// These environment variables are no longer needed because:
|
|
305
|
+
// 1. All production methods use *ByNetwork variants
|
|
306
|
+
// 2. Node racing system provides superior reliability and performance
|
|
307
|
+
// 3. 1200+ validated nodes vs single environment variable endpoint
|
|
308
|
+
// 4. Automatic failover, health tracking, and tier-based routing
|
|
313
309
|
log.info(tag, 'ETH network module initialized successfully');
|
|
310
|
+
log.info(tag, '✅ Using node racing system for all operations (no env dependencies)');
|
|
314
311
|
};
|
|
315
312
|
exports.init = init;
|
|
316
313
|
/**
|
|
@@ -347,8 +344,12 @@ const addNodes = function (nodeList) {
|
|
|
347
344
|
exports.addNodes = addNodes;
|
|
348
345
|
/**
|
|
349
346
|
* Get nonce for address
|
|
347
|
+
* @deprecated Use getNonceByNetwork instead - this method relies on environment variables
|
|
348
|
+
* and does not use the node racing/fallback system
|
|
350
349
|
*/
|
|
351
350
|
const getNonce = async function (address) {
|
|
351
|
+
if (!PROVIDER)
|
|
352
|
+
throw Error('PROVIDER not initialized - use getNonceByNetwork instead');
|
|
352
353
|
return await PROVIDER.getTransactionCount(address, 'pending');
|
|
353
354
|
};
|
|
354
355
|
exports.getNonce = getNonce;
|
|
@@ -357,23 +358,49 @@ exports.getNonce = getNonce;
|
|
|
357
358
|
*/
|
|
358
359
|
const getNonceByNetwork = async function (networkId, address) {
|
|
359
360
|
let tag = TAG + ' | getNonceByNetwork | ';
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const provider = new ethers.providers.JsonRpcProvider(node.service);
|
|
365
|
-
return await withTimeout(provider.getTransactionCount(address, 'pending'), RPC_TIMEOUT_MS, `Nonce fetch timeout after ${RPC_TIMEOUT_MS}ms`);
|
|
361
|
+
// Get ALL nodes for this network
|
|
362
|
+
let nodes = NODES.filter((n) => n.networkId === networkId);
|
|
363
|
+
if (!nodes || nodes.length === 0) {
|
|
364
|
+
throw Error(`No nodes found for networkId: ${networkId}`);
|
|
366
365
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
366
|
+
// Extract chain ID from networkId (e.g., "eip155:1" -> 1)
|
|
367
|
+
let chainId;
|
|
368
|
+
if (networkId.includes(':')) {
|
|
369
|
+
chainId = parseInt(networkId.split(':')[1]);
|
|
370
370
|
}
|
|
371
|
+
// Try each node until one succeeds
|
|
372
|
+
let lastError = null;
|
|
373
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
374
|
+
const node = nodes[i];
|
|
375
|
+
try {
|
|
376
|
+
// Use StaticJsonRpcProvider to skip network auto-detection
|
|
377
|
+
const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
|
|
378
|
+
const nonce = await withTimeout(provider.getTransactionCount(address, 'pending'), RPC_TIMEOUT_MS, `Nonce fetch timeout after ${RPC_TIMEOUT_MS}ms for ${node.service.substring(0, 50)}`);
|
|
379
|
+
// Success! Log which node worked (only if not the first one)
|
|
380
|
+
if (i > 0) {
|
|
381
|
+
log.info(tag, `Successfully fetched nonce from node ${i + 1}/${nodes.length}: ${node.service.substring(0, 50)}`);
|
|
382
|
+
}
|
|
383
|
+
return nonce;
|
|
384
|
+
}
|
|
385
|
+
catch (e) {
|
|
386
|
+
lastError = e;
|
|
387
|
+
log.warn(tag, `Node ${i + 1}/${nodes.length} failed (${node.service.substring(0, 50)}): ${e.message}`);
|
|
388
|
+
// Continue to next node
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// All nodes failed
|
|
392
|
+
log.error(tag, `All ${nodes.length} nodes failed for ${networkId}`);
|
|
393
|
+
throw lastError || Error(`Failed to get nonce from any node for ${networkId}`);
|
|
371
394
|
};
|
|
372
395
|
exports.getNonceByNetwork = getNonceByNetwork;
|
|
373
396
|
/**
|
|
374
397
|
* Get current gas price
|
|
398
|
+
* @deprecated Use getGasPriceByNetwork instead - this method relies on environment variables
|
|
399
|
+
* and does not use the node racing/fallback system
|
|
375
400
|
*/
|
|
376
401
|
const getGasPrice = async function () {
|
|
402
|
+
if (!PROVIDER)
|
|
403
|
+
throw Error('PROVIDER not initialized - use getGasPriceByNetwork instead');
|
|
377
404
|
return await PROVIDER.getGasPrice();
|
|
378
405
|
};
|
|
379
406
|
exports.getGasPrice = getGasPrice;
|
|
@@ -471,9 +498,13 @@ const estimateFee = async function (asset, params) {
|
|
|
471
498
|
exports.estimateFee = estimateFee;
|
|
472
499
|
/**
|
|
473
500
|
* Get ETH balance for address
|
|
501
|
+
* @deprecated Use getBalanceAddressByNetwork instead - this method relies on environment variables
|
|
502
|
+
* and does not use the node racing/fallback system with health tracking
|
|
474
503
|
*/
|
|
475
504
|
const getBalance = async function (address) {
|
|
476
505
|
let tag = TAG + ' | getBalance | ';
|
|
506
|
+
if (!PROVIDER)
|
|
507
|
+
throw Error('PROVIDER not initialized - use getBalanceAddressByNetwork instead');
|
|
477
508
|
try {
|
|
478
509
|
const balance = await PROVIDER.getBalance(address);
|
|
479
510
|
return ethers.utils.formatEther(balance);
|
|
@@ -486,9 +517,12 @@ const getBalance = async function (address) {
|
|
|
486
517
|
exports.getBalance = getBalance;
|
|
487
518
|
/**
|
|
488
519
|
* Get balances for multiple addresses
|
|
520
|
+
* @deprecated Use getBalanceAddressByNetwork in a loop instead - this method relies on environment variables
|
|
489
521
|
*/
|
|
490
522
|
const getBalances = async function (addresses) {
|
|
491
523
|
let tag = TAG + ' | getBalances | ';
|
|
524
|
+
if (!PROVIDER)
|
|
525
|
+
throw Error('PROVIDER not initialized - use getBalanceAddressByNetwork instead');
|
|
492
526
|
try {
|
|
493
527
|
const promises = addresses.map(addr => PROVIDER.getBalance(addr));
|
|
494
528
|
const balances = await Promise.all(promises);
|
|
@@ -671,29 +705,40 @@ const getBalanceAddressByNetwork = async function (networkId, address) {
|
|
|
671
705
|
}
|
|
672
706
|
catch (e) {
|
|
673
707
|
log.warn(tag, `❌ Node ${batchStartIndex + index + 1}/${nodes.length} failed: ${node.service.substring(0, 50)} - ${e.message}`);
|
|
674
|
-
// CRITICAL FIX: Don't mark nodes dead for validation errors
|
|
675
|
-
// Only mark dead for connectivity/timeout issues
|
|
708
|
+
// CRITICAL FIX: Don't mark nodes dead for validation or SSL errors
|
|
709
|
+
// Only mark dead for persistent connectivity/timeout issues
|
|
676
710
|
const isValidationError = e.message && (e.message.includes('invalid address') ||
|
|
677
711
|
e.message.includes('INVALID_ARGUMENT') ||
|
|
678
712
|
e.message.includes('bad address checksum'));
|
|
679
|
-
|
|
713
|
+
const isSSLError = e.message && (e.message.includes('UNABLE_TO_GET_ISSUER_CERT') ||
|
|
714
|
+
e.message.includes('certificate') ||
|
|
715
|
+
e.message.includes('SSL') ||
|
|
716
|
+
e.message.includes('TLS') ||
|
|
717
|
+
e.code === 'UNABLE_TO_GET_ISSUER_CERT' ||
|
|
718
|
+
e.code === 'CERT_HAS_EXPIRED' ||
|
|
719
|
+
e.code === 'SELF_SIGNED_CERT_IN_CHAIN');
|
|
720
|
+
if (!isValidationError && !isSSLError) {
|
|
680
721
|
markNodeDead(node.service, node.tier);
|
|
681
722
|
await NodeHealth.recordFailure(node.service, node.networkId);
|
|
682
723
|
}
|
|
683
|
-
else {
|
|
724
|
+
else if (isValidationError) {
|
|
684
725
|
log.warn(tag, `Skipping node death marking - validation error (not node's fault)`);
|
|
685
726
|
}
|
|
727
|
+
else if (isSSLError) {
|
|
728
|
+
log.warn(tag, `Skipping node death marking - SSL/TLS error (might be temporary certificate issue)`);
|
|
729
|
+
}
|
|
686
730
|
throw e;
|
|
687
731
|
}
|
|
688
732
|
});
|
|
689
|
-
//
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
log.warn(tag, `Batch ${Math.floor(batchStartIndex / RACE_BATCH_SIZE) + 1} failed, trying next batch...`);
|
|
733
|
+
// Use allSettled to let all nodes complete, return first success
|
|
734
|
+
const results = await Promise.allSettled(racePromises);
|
|
735
|
+
// Find first successful result
|
|
736
|
+
const successResult = results.find(r => r.status === 'fulfilled' && r.value.success);
|
|
737
|
+
if (successResult && successResult.status === 'fulfilled') {
|
|
738
|
+
return successResult.value.result;
|
|
696
739
|
}
|
|
740
|
+
// All nodes in this batch failed, try next batch
|
|
741
|
+
log.warn(tag, `Batch ${Math.floor(batchStartIndex / RACE_BATCH_SIZE) + 1} failed (${results.filter(r => r.status === 'rejected').length}/${results.length} errors), trying next batch...`);
|
|
697
742
|
batchStartIndex += RACE_BATCH_SIZE;
|
|
698
743
|
}
|
|
699
744
|
// All batches failed
|
|
@@ -749,21 +794,40 @@ const getBalanceTokenByNetwork = async function (networkId, address, tokenAddres
|
|
|
749
794
|
return { success: true, result, nodeUrl: node.service };
|
|
750
795
|
}
|
|
751
796
|
catch (e) {
|
|
752
|
-
//
|
|
797
|
+
// Check error type before marking node dead
|
|
753
798
|
log.warn(tag, `❌ Node ${batchStartIndex + index + 1}/${nodes.length} failed: ${node.service.substring(0, 50)} - ${e.message}`);
|
|
754
|
-
|
|
799
|
+
// Don't mark nodes dead for validation or SSL errors
|
|
800
|
+
const isValidationError = e.message && (e.message.includes('invalid address') ||
|
|
801
|
+
e.message.includes('INVALID_ARGUMENT') ||
|
|
802
|
+
e.message.includes('bad address checksum'));
|
|
803
|
+
const isSSLError = e.message && (e.message.includes('UNABLE_TO_GET_ISSUER_CERT') ||
|
|
804
|
+
e.message.includes('certificate') ||
|
|
805
|
+
e.message.includes('SSL') ||
|
|
806
|
+
e.message.includes('TLS') ||
|
|
807
|
+
e.code === 'UNABLE_TO_GET_ISSUER_CERT' ||
|
|
808
|
+
e.code === 'CERT_HAS_EXPIRED' ||
|
|
809
|
+
e.code === 'SELF_SIGNED_CERT_IN_CHAIN');
|
|
810
|
+
if (!isValidationError && !isSSLError) {
|
|
811
|
+
markNodeDead(node.service, node.tier);
|
|
812
|
+
}
|
|
813
|
+
else if (isValidationError) {
|
|
814
|
+
log.warn(tag, `Skipping node death marking - validation error`);
|
|
815
|
+
}
|
|
816
|
+
else if (isSSLError) {
|
|
817
|
+
log.warn(tag, `Skipping node death marking - SSL/TLS error (might be temporary)`);
|
|
818
|
+
}
|
|
755
819
|
throw e;
|
|
756
820
|
}
|
|
757
821
|
});
|
|
758
|
-
//
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
// All nodes in this batch failed, try next batch
|
|
765
|
-
log.warn(tag, `Batch ${Math.floor(batchStartIndex / RACE_BATCH_SIZE) + 1} failed, trying next batch...`);
|
|
822
|
+
// Use allSettled to let all nodes complete, return first success
|
|
823
|
+
const results = await Promise.allSettled(racePromises);
|
|
824
|
+
// Find first successful result
|
|
825
|
+
const successResult = results.find(r => r.status === 'fulfilled' && r.value.success);
|
|
826
|
+
if (successResult && successResult.status === 'fulfilled') {
|
|
827
|
+
return successResult.value.result;
|
|
766
828
|
}
|
|
829
|
+
// All nodes in this batch failed, try next batch
|
|
830
|
+
log.warn(tag, `Batch ${Math.floor(batchStartIndex / RACE_BATCH_SIZE) + 1} failed (${results.filter(r => r.status === 'rejected').length}/${results.length} errors), trying next batch...`);
|
|
767
831
|
batchStartIndex += RACE_BATCH_SIZE;
|
|
768
832
|
}
|
|
769
833
|
// All batches failed
|
|
@@ -793,27 +857,41 @@ exports.getBalanceTokensByNetwork = getBalanceTokensByNetwork;
|
|
|
793
857
|
*/
|
|
794
858
|
const getTokenDecimalsByNetwork = async function (networkId, tokenAddress) {
|
|
795
859
|
let tag = TAG + ' | getTokenDecimalsByNetwork | ';
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
throw Error(`Node not found for networkId: ${networkId}`);
|
|
801
|
-
// Extract chain ID from networkId (e.g., "eip155:1" -> 1)
|
|
802
|
-
let chainId;
|
|
803
|
-
if (networkId.includes(':')) {
|
|
804
|
-
chainId = parseInt(networkId.split(':')[1]);
|
|
805
|
-
}
|
|
806
|
-
// Use StaticJsonRpcProvider to skip network auto-detection
|
|
807
|
-
const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
|
|
808
|
-
const contract = new ethers.Contract(tokenAddress, ERC20ABI, provider);
|
|
809
|
-
const decimals = await contract.decimals();
|
|
810
|
-
// Return as number for easy use
|
|
811
|
-
return Number(decimals);
|
|
860
|
+
// Get ALL nodes for this network
|
|
861
|
+
let nodes = NODES.filter((n) => n.networkId === networkId);
|
|
862
|
+
if (!nodes || nodes.length === 0) {
|
|
863
|
+
throw Error(`No nodes found for networkId: ${networkId}`);
|
|
812
864
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
865
|
+
// Extract chain ID from networkId (e.g., "eip155:1" -> 1)
|
|
866
|
+
let chainId;
|
|
867
|
+
if (networkId.includes(':')) {
|
|
868
|
+
chainId = parseInt(networkId.split(':')[1]);
|
|
869
|
+
}
|
|
870
|
+
// Try each node until one succeeds
|
|
871
|
+
let lastError = null;
|
|
872
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
873
|
+
const node = nodes[i];
|
|
874
|
+
try {
|
|
875
|
+
// Use StaticJsonRpcProvider to skip network auto-detection
|
|
876
|
+
const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
|
|
877
|
+
const contract = new ethers.Contract(tokenAddress, ERC20ABI, provider);
|
|
878
|
+
const decimals = await withTimeout(contract.decimals(), RPC_TIMEOUT_MS, `Token decimals fetch timeout after ${RPC_TIMEOUT_MS}ms for ${node.service.substring(0, 50)}`);
|
|
879
|
+
// Success! Log which node worked (only if not the first one)
|
|
880
|
+
if (i > 0) {
|
|
881
|
+
log.info(tag, `Successfully fetched decimals from node ${i + 1}/${nodes.length}: ${node.service.substring(0, 50)}`);
|
|
882
|
+
}
|
|
883
|
+
// Return as number for easy use
|
|
884
|
+
return Number(decimals);
|
|
885
|
+
}
|
|
886
|
+
catch (e) {
|
|
887
|
+
lastError = e;
|
|
888
|
+
log.warn(tag, `Node ${i + 1}/${nodes.length} failed (${node.service.substring(0, 50)}): ${e.message}`);
|
|
889
|
+
// Continue to next node
|
|
890
|
+
}
|
|
816
891
|
}
|
|
892
|
+
// All nodes failed
|
|
893
|
+
log.error(tag, `All ${nodes.length} nodes failed for ${networkId}`);
|
|
894
|
+
throw lastError || Error(`Failed to get token decimals from any node for ${networkId}`);
|
|
817
895
|
};
|
|
818
896
|
exports.getTokenDecimalsByNetwork = getTokenDecimalsByNetwork;
|
|
819
897
|
/**
|
|
@@ -890,25 +968,46 @@ const getTokenMetadata = async function (networkId, contractAddress, userAddress
|
|
|
890
968
|
return { success: true, result: metadata, nodeUrl: node.service };
|
|
891
969
|
}
|
|
892
970
|
catch (e) {
|
|
971
|
+
// Check error type before marking node dead
|
|
893
972
|
log.warn(tag, `❌ Node ${batchStartIndex + index + 1}/${nodes.length} failed: ${node.service.substring(0, 50)} - ${e.message}`);
|
|
894
|
-
|
|
973
|
+
// Don't mark nodes dead for validation or SSL errors
|
|
974
|
+
const isValidationError = e.message && (e.message.includes('invalid address') ||
|
|
975
|
+
e.message.includes('INVALID_ARGUMENT') ||
|
|
976
|
+
e.message.includes('bad address checksum'));
|
|
977
|
+
const isSSLError = e.message && (e.message.includes('UNABLE_TO_GET_ISSUER_CERT') ||
|
|
978
|
+
e.message.includes('certificate') ||
|
|
979
|
+
e.message.includes('SSL') ||
|
|
980
|
+
e.message.includes('TLS') ||
|
|
981
|
+
e.code === 'UNABLE_TO_GET_ISSUER_CERT' ||
|
|
982
|
+
e.code === 'CERT_HAS_EXPIRED' ||
|
|
983
|
+
e.code === 'SELF_SIGNED_CERT_IN_CHAIN');
|
|
984
|
+
if (!isValidationError && !isSSLError) {
|
|
985
|
+
markNodeDead(node.service, node.tier);
|
|
986
|
+
}
|
|
987
|
+
else if (isValidationError) {
|
|
988
|
+
log.warn(tag, `Skipping node death marking - validation error`);
|
|
989
|
+
}
|
|
990
|
+
else if (isSSLError) {
|
|
991
|
+
log.warn(tag, `Skipping node death marking - SSL/TLS error (might be temporary)`);
|
|
992
|
+
}
|
|
895
993
|
throw e;
|
|
896
994
|
}
|
|
897
995
|
});
|
|
898
|
-
//
|
|
899
|
-
|
|
900
|
-
|
|
996
|
+
// Use allSettled to let all nodes complete, return first success
|
|
997
|
+
const results = await Promise.allSettled(racePromises);
|
|
998
|
+
// Find first successful result
|
|
999
|
+
const successResult = results.find(r => r.status === 'fulfilled' && r.value.success);
|
|
1000
|
+
if (successResult && successResult.status === 'fulfilled') {
|
|
901
1001
|
log.info(tag, `Token metadata fetched successfully:`, {
|
|
902
|
-
name:
|
|
903
|
-
symbol:
|
|
904
|
-
decimals:
|
|
905
|
-
balance:
|
|
1002
|
+
name: successResult.value.result.name,
|
|
1003
|
+
symbol: successResult.value.result.symbol,
|
|
1004
|
+
decimals: successResult.value.result.decimals,
|
|
1005
|
+
balance: successResult.value.result.balance
|
|
906
1006
|
});
|
|
907
|
-
return
|
|
908
|
-
}
|
|
909
|
-
catch (e) {
|
|
910
|
-
log.warn(tag, `Batch ${Math.floor(batchStartIndex / RACE_BATCH_SIZE) + 1} failed, trying next batch...`);
|
|
1007
|
+
return successResult.value.result;
|
|
911
1008
|
}
|
|
1009
|
+
// All nodes in this batch failed, try next batch
|
|
1010
|
+
log.warn(tag, `Batch ${Math.floor(batchStartIndex / RACE_BATCH_SIZE) + 1} failed (${results.filter(r => r.status === 'rejected').length}/${results.length} errors), trying next batch...`);
|
|
912
1011
|
batchStartIndex += RACE_BATCH_SIZE;
|
|
913
1012
|
}
|
|
914
1013
|
// All batches failed
|
|
@@ -930,23 +1029,38 @@ exports.getTokenMetadataByNetwork = getTokenMetadataByNetwork;
|
|
|
930
1029
|
*/
|
|
931
1030
|
const estimateGasByNetwork = async function (networkId, transaction) {
|
|
932
1031
|
let tag = TAG + ' | estimateGasByNetwork | ';
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
// Extract chain ID from networkId
|
|
938
|
-
let chainId;
|
|
939
|
-
if (networkId.includes(':')) {
|
|
940
|
-
chainId = parseInt(networkId.split(':')[1]);
|
|
941
|
-
}
|
|
942
|
-
const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
|
|
943
|
-
const gasEstimate = await provider.estimateGas(transaction);
|
|
944
|
-
return gasEstimate;
|
|
1032
|
+
// Get ALL nodes for this network
|
|
1033
|
+
let nodes = NODES.filter((n) => n.networkId === networkId);
|
|
1034
|
+
if (!nodes || nodes.length === 0) {
|
|
1035
|
+
throw Error(`No nodes found for networkId: ${networkId}`);
|
|
945
1036
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1037
|
+
// Extract chain ID from networkId
|
|
1038
|
+
let chainId;
|
|
1039
|
+
if (networkId.includes(':')) {
|
|
1040
|
+
chainId = parseInt(networkId.split(':')[1]);
|
|
949
1041
|
}
|
|
1042
|
+
// Try each node until one succeeds
|
|
1043
|
+
let lastError = null;
|
|
1044
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1045
|
+
const node = nodes[i];
|
|
1046
|
+
try {
|
|
1047
|
+
const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
|
|
1048
|
+
const gasEstimate = await withTimeout(provider.estimateGas(transaction), RPC_TIMEOUT_MS, `Gas estimate timeout after ${RPC_TIMEOUT_MS}ms for ${node.service.substring(0, 50)}`);
|
|
1049
|
+
// Success! Log which node worked (only if not the first one)
|
|
1050
|
+
if (i > 0) {
|
|
1051
|
+
log.info(tag, `Successfully estimated gas from node ${i + 1}/${nodes.length}: ${node.service.substring(0, 50)}`);
|
|
1052
|
+
}
|
|
1053
|
+
return gasEstimate;
|
|
1054
|
+
}
|
|
1055
|
+
catch (e) {
|
|
1056
|
+
lastError = e;
|
|
1057
|
+
log.warn(tag, `Node ${i + 1}/${nodes.length} failed (${node.service.substring(0, 50)}): ${e.message}`);
|
|
1058
|
+
// Continue to next node
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
// All nodes failed
|
|
1062
|
+
log.error(tag, `All ${nodes.length} nodes failed for ${networkId}`);
|
|
1063
|
+
throw lastError || Error(`Failed to estimate gas from any node for ${networkId}`);
|
|
950
1064
|
};
|
|
951
1065
|
exports.estimateGasByNetwork = estimateGasByNetwork;
|
|
952
1066
|
/**
|
|
@@ -954,38 +1068,55 @@ exports.estimateGasByNetwork = estimateGasByNetwork;
|
|
|
954
1068
|
*/
|
|
955
1069
|
const broadcastByNetwork = async function (networkId, signedTx) {
|
|
956
1070
|
let tag = TAG + ' | broadcastByNetwork | ';
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
throw Error(`Node not found for networkId: ${networkId}`);
|
|
961
|
-
// Extract chain ID from networkId
|
|
962
|
-
let chainId;
|
|
963
|
-
if (networkId.includes(':')) {
|
|
964
|
-
chainId = parseInt(networkId.split(':')[1]);
|
|
965
|
-
}
|
|
966
|
-
const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
|
|
967
|
-
const result = await provider.sendTransaction(signedTx);
|
|
968
|
-
// Return in expected format for pioneer-server
|
|
969
|
-
return {
|
|
970
|
-
success: true,
|
|
971
|
-
txid: result.hash,
|
|
972
|
-
transactionHash: result.hash,
|
|
973
|
-
from: result.from,
|
|
974
|
-
to: result.to,
|
|
975
|
-
nonce: result.nonce,
|
|
976
|
-
gasLimit: result.gasLimit?.toString(),
|
|
977
|
-
gasPrice: result.gasPrice?.toString(),
|
|
978
|
-
chainId: result.chainId
|
|
979
|
-
};
|
|
980
|
-
}
|
|
981
|
-
catch (e) {
|
|
982
|
-
log.error(tag, e);
|
|
983
|
-
// Return error format expected by server
|
|
1071
|
+
// Get ALL nodes for this network
|
|
1072
|
+
let nodes = NODES.filter((n) => n.networkId === networkId);
|
|
1073
|
+
if (!nodes || nodes.length === 0) {
|
|
984
1074
|
return {
|
|
985
1075
|
success: false,
|
|
986
|
-
error:
|
|
1076
|
+
error: `No nodes found for networkId: ${networkId}`
|
|
987
1077
|
};
|
|
988
1078
|
}
|
|
1079
|
+
// Extract chain ID from networkId
|
|
1080
|
+
let chainId;
|
|
1081
|
+
if (networkId.includes(':')) {
|
|
1082
|
+
chainId = parseInt(networkId.split(':')[1]);
|
|
1083
|
+
}
|
|
1084
|
+
// Try each node until one succeeds
|
|
1085
|
+
let lastError = null;
|
|
1086
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1087
|
+
const node = nodes[i];
|
|
1088
|
+
try {
|
|
1089
|
+
const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
|
|
1090
|
+
const result = await withTimeout(provider.sendTransaction(signedTx), RPC_TIMEOUT_MS, `Broadcast timeout after ${RPC_TIMEOUT_MS}ms for ${node.service.substring(0, 50)}`);
|
|
1091
|
+
// Success! Log which node worked (only if not the first one)
|
|
1092
|
+
if (i > 0) {
|
|
1093
|
+
log.info(tag, `Successfully broadcasted from node ${i + 1}/${nodes.length}: ${node.service.substring(0, 50)}`);
|
|
1094
|
+
}
|
|
1095
|
+
// Return in expected format for pioneer-server
|
|
1096
|
+
return {
|
|
1097
|
+
success: true,
|
|
1098
|
+
txid: result.hash,
|
|
1099
|
+
transactionHash: result.hash,
|
|
1100
|
+
from: result.from,
|
|
1101
|
+
to: result.to,
|
|
1102
|
+
nonce: result.nonce,
|
|
1103
|
+
gasLimit: result.gasLimit?.toString(),
|
|
1104
|
+
gasPrice: result.gasPrice?.toString(),
|
|
1105
|
+
chainId: result.chainId
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
catch (e) {
|
|
1109
|
+
lastError = e;
|
|
1110
|
+
log.warn(tag, `Node ${i + 1}/${nodes.length} failed (${node.service.substring(0, 50)}): ${e.message}`);
|
|
1111
|
+
// Continue to next node
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
// All nodes failed
|
|
1115
|
+
log.error(tag, `All ${nodes.length} nodes failed for ${networkId}`);
|
|
1116
|
+
return {
|
|
1117
|
+
success: false,
|
|
1118
|
+
error: lastError?.message || `Failed to broadcast from any node for ${networkId}`
|
|
1119
|
+
};
|
|
989
1120
|
};
|
|
990
1121
|
exports.broadcastByNetwork = broadcastByNetwork;
|
|
991
1122
|
/**
|
|
@@ -993,25 +1124,40 @@ exports.broadcastByNetwork = broadcastByNetwork;
|
|
|
993
1124
|
*/
|
|
994
1125
|
const getTransactionByNetwork = async function (networkId, txid) {
|
|
995
1126
|
let tag = TAG + ' | getTransactionByNetwork | ';
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
// Extract chain ID from networkId
|
|
1001
|
-
let chainId;
|
|
1002
|
-
if (networkId.includes(':')) {
|
|
1003
|
-
chainId = parseInt(networkId.split(':')[1]);
|
|
1004
|
-
}
|
|
1005
|
-
const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
|
|
1006
|
-
log.info(tag, `Fetching transaction ${txid} on ${networkId}`);
|
|
1007
|
-
const tx = await provider.getTransaction(txid);
|
|
1008
|
-
const receipt = await provider.getTransactionReceipt(txid);
|
|
1009
|
-
return { tx, receipt };
|
|
1127
|
+
// Get ALL nodes for this network
|
|
1128
|
+
let nodes = NODES.filter((n) => n.networkId === networkId);
|
|
1129
|
+
if (!nodes || nodes.length === 0) {
|
|
1130
|
+
throw Error(`No nodes found for networkId: ${networkId}`);
|
|
1010
1131
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1132
|
+
// Extract chain ID from networkId
|
|
1133
|
+
let chainId;
|
|
1134
|
+
if (networkId.includes(':')) {
|
|
1135
|
+
chainId = parseInt(networkId.split(':')[1]);
|
|
1014
1136
|
}
|
|
1137
|
+
log.info(tag, `Fetching transaction ${txid} on ${networkId}`);
|
|
1138
|
+
// Try each node until one succeeds
|
|
1139
|
+
let lastError = null;
|
|
1140
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1141
|
+
const node = nodes[i];
|
|
1142
|
+
try {
|
|
1143
|
+
const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
|
|
1144
|
+
const tx = await withTimeout(provider.getTransaction(txid), RPC_TIMEOUT_MS, `Get transaction timeout after ${RPC_TIMEOUT_MS}ms for ${node.service.substring(0, 50)}`);
|
|
1145
|
+
const receipt = await withTimeout(provider.getTransactionReceipt(txid), RPC_TIMEOUT_MS, `Get receipt timeout after ${RPC_TIMEOUT_MS}ms for ${node.service.substring(0, 50)}`);
|
|
1146
|
+
// Success! Log which node worked (only if not the first one)
|
|
1147
|
+
if (i > 0) {
|
|
1148
|
+
log.info(tag, `Successfully fetched transaction from node ${i + 1}/${nodes.length}: ${node.service.substring(0, 50)}`);
|
|
1149
|
+
}
|
|
1150
|
+
return { tx, receipt };
|
|
1151
|
+
}
|
|
1152
|
+
catch (e) {
|
|
1153
|
+
lastError = e;
|
|
1154
|
+
log.warn(tag, `Node ${i + 1}/${nodes.length} failed (${node.service.substring(0, 50)}): ${e.message}`);
|
|
1155
|
+
// Continue to next node
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
// All nodes failed
|
|
1159
|
+
log.error(tag, `All ${nodes.length} nodes failed for ${networkId}`);
|
|
1160
|
+
throw lastError || Error(`Failed to get transaction from any node for ${networkId}`);
|
|
1015
1161
|
};
|
|
1016
1162
|
exports.getTransactionByNetwork = getTransactionByNetwork;
|
|
1017
1163
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pioneer-platform/eth-network",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.14.3",
|
|
4
4
|
"main": "./lib/index.js",
|
|
5
5
|
"types": "./lib/index.d.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@ethersproject/abstract-provider": "^5.8.0",
|
|
19
19
|
"@ethersproject/bignumber": "^5.8.0",
|
|
20
20
|
"@ethersproject/providers": "^5.8.0",
|
|
21
|
-
"@pioneer-platform/blockbook": "^8.
|
|
21
|
+
"@pioneer-platform/blockbook": "^8.12.0",
|
|
22
22
|
"@pioneer-platform/loggerdog": "^8.11.0",
|
|
23
23
|
"@pioneer-platform/nodes": "^8.11.10",
|
|
24
24
|
"@pioneer-platform/pioneer-caip": "^9.10.0",
|