@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.
@@ -1,2 +1 @@
1
-
2
- $ tsc -p .
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
- // Provider instances
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
- if (!settings) {
295
- // Use default mainnet
296
- PROVIDER = new ethers.providers.JsonRpcProvider(process.env['PARITY_ARCHIVE_NODE'] || 'https://eth.llamarpc.com');
297
- PROVIDER_BASE = new ethers.providers.JsonRpcProvider(process.env['BASE_NODE'] || 'https://base.llamarpc.com');
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
- try {
361
- let node = NODES.find((n) => n.networkId === networkId);
362
- if (!node)
363
- throw Error(`Node not found for networkId: ${networkId}`);
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
- catch (e) {
368
- log.error(tag, e);
369
- throw e;
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
- if (!isValidationError) {
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
- // Race this batch - return first successful result
690
- try {
691
- const winner = await Promise.race(racePromises.map(p => p.catch(e => Promise.reject(e))));
692
- return winner.result;
693
- }
694
- catch (e) {
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
- // Mark node as dead and continue
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
- markNodeDead(node.service, node.tier);
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
- // Race this batch - return first successful result
759
- try {
760
- const winner = await Promise.race(racePromises.map(p => p.catch(e => Promise.reject(e))));
761
- return winner.result;
762
- }
763
- catch (e) {
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
- try {
797
- // Find the node for this network
798
- let node = NODES.find((n) => n.networkId === networkId);
799
- if (!node)
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
- catch (e) {
814
- log.error(tag, 'Failed to fetch decimals for', tokenAddress, 'on', networkId, ':', e);
815
- throw e;
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
- markNodeDead(node.service, node.tier);
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
- // Race this batch - return first successful result
899
- try {
900
- const winner = await Promise.race(racePromises.map(p => p.catch(e => Promise.reject(e))));
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: winner.result.name,
903
- symbol: winner.result.symbol,
904
- decimals: winner.result.decimals,
905
- balance: winner.result.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 winner.result;
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
- try {
934
- let node = NODES.find((n) => n.networkId === networkId);
935
- if (!node)
936
- throw Error(`Node not found for networkId: ${networkId}`);
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
- catch (e) {
947
- log.error(tag, e);
948
- throw e;
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
- try {
958
- let node = NODES.find((n) => n.networkId === networkId);
959
- if (!node)
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: e.message || e.toString()
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
- try {
997
- let node = NODES.find((n) => n.networkId === networkId);
998
- if (!node)
999
- throw Error(`Node not found for networkId: ${networkId}`);
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
- catch (e) {
1012
- log.error(tag, e);
1013
- throw e;
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.13.16",
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.11.6",
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",