@pioneer-platform/eth-network 8.14.0 → 8.14.2

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/index.js +255 -127
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @pioneer-platform/eth-network
2
2
 
3
+ ## 8.14.2
4
+
5
+ ### Patch Changes
6
+
7
+ - fix: Add node racing to all eth-network \*ByNetwork functions
8
+
9
+ ## 8.14.1
10
+
11
+ ### Patch Changes
12
+
13
+ - cache work
14
+
3
15
  ## 8.14.0
4
16
 
5
17
  ### Minor Changes
package/lib/index.js CHANGED
@@ -358,23 +358,39 @@ exports.getNonce = getNonce;
358
358
  */
359
359
  const getNonceByNetwork = async function (networkId, address) {
360
360
  let tag = TAG + ' | getNonceByNetwork | ';
361
- try {
362
- let node = NODES.find((n) => n.networkId === networkId);
363
- if (!node)
364
- throw Error(`Node not found for networkId: ${networkId}`);
365
- // Extract chain ID from networkId (e.g., "eip155:1" -> 1)
366
- let chainId;
367
- if (networkId.includes(':')) {
368
- chainId = parseInt(networkId.split(':')[1]);
369
- }
370
- // Use StaticJsonRpcProvider to skip network auto-detection
371
- const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
372
- 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}`);
373
365
  }
374
- catch (e) {
375
- log.error(tag, e);
376
- 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]);
377
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}`);
378
394
  };
379
395
  exports.getNonceByNetwork = getNonceByNetwork;
380
396
  /**
@@ -689,29 +705,40 @@ const getBalanceAddressByNetwork = async function (networkId, address) {
689
705
  }
690
706
  catch (e) {
691
707
  log.warn(tag, `❌ Node ${batchStartIndex + index + 1}/${nodes.length} failed: ${node.service.substring(0, 50)} - ${e.message}`);
692
- // CRITICAL FIX: Don't mark nodes dead for validation errors
693
- // 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
694
710
  const isValidationError = e.message && (e.message.includes('invalid address') ||
695
711
  e.message.includes('INVALID_ARGUMENT') ||
696
712
  e.message.includes('bad address checksum'));
697
- 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) {
698
721
  markNodeDead(node.service, node.tier);
699
722
  await NodeHealth.recordFailure(node.service, node.networkId);
700
723
  }
701
- else {
724
+ else if (isValidationError) {
702
725
  log.warn(tag, `Skipping node death marking - validation error (not node's fault)`);
703
726
  }
727
+ else if (isSSLError) {
728
+ log.warn(tag, `Skipping node death marking - SSL/TLS error (might be temporary certificate issue)`);
729
+ }
704
730
  throw e;
705
731
  }
706
732
  });
707
- // Race this batch - return first successful result
708
- try {
709
- const winner = await Promise.race(racePromises.map(p => p.catch(e => Promise.reject(e))));
710
- return winner.result;
711
- }
712
- catch (e) {
713
- 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;
714
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...`);
715
742
  batchStartIndex += RACE_BATCH_SIZE;
716
743
  }
717
744
  // All batches failed
@@ -767,21 +794,40 @@ const getBalanceTokenByNetwork = async function (networkId, address, tokenAddres
767
794
  return { success: true, result, nodeUrl: node.service };
768
795
  }
769
796
  catch (e) {
770
- // Mark node as dead and continue
797
+ // Check error type before marking node dead
771
798
  log.warn(tag, `❌ Node ${batchStartIndex + index + 1}/${nodes.length} failed: ${node.service.substring(0, 50)} - ${e.message}`);
772
- 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
+ }
773
819
  throw e;
774
820
  }
775
821
  });
776
- // Race this batch - return first successful result
777
- try {
778
- const winner = await Promise.race(racePromises.map(p => p.catch(e => Promise.reject(e))));
779
- return winner.result;
780
- }
781
- catch (e) {
782
- // All nodes in this batch failed, try next batch
783
- 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;
784
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...`);
785
831
  batchStartIndex += RACE_BATCH_SIZE;
786
832
  }
787
833
  // All batches failed
@@ -811,27 +857,41 @@ exports.getBalanceTokensByNetwork = getBalanceTokensByNetwork;
811
857
  */
812
858
  const getTokenDecimalsByNetwork = async function (networkId, tokenAddress) {
813
859
  let tag = TAG + ' | getTokenDecimalsByNetwork | ';
814
- try {
815
- // Find the node for this network
816
- let node = NODES.find((n) => n.networkId === networkId);
817
- if (!node)
818
- throw Error(`Node not found for networkId: ${networkId}`);
819
- // Extract chain ID from networkId (e.g., "eip155:1" -> 1)
820
- let chainId;
821
- if (networkId.includes(':')) {
822
- chainId = parseInt(networkId.split(':')[1]);
823
- }
824
- // Use StaticJsonRpcProvider to skip network auto-detection
825
- const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
826
- const contract = new ethers.Contract(tokenAddress, ERC20ABI, provider);
827
- const decimals = await contract.decimals();
828
- // Return as number for easy use
829
- 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}`);
830
864
  }
831
- catch (e) {
832
- log.error(tag, 'Failed to fetch decimals for', tokenAddress, 'on', networkId, ':', e);
833
- 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
+ }
834
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}`);
835
895
  };
836
896
  exports.getTokenDecimalsByNetwork = getTokenDecimalsByNetwork;
837
897
  /**
@@ -908,25 +968,46 @@ const getTokenMetadata = async function (networkId, contractAddress, userAddress
908
968
  return { success: true, result: metadata, nodeUrl: node.service };
909
969
  }
910
970
  catch (e) {
971
+ // Check error type before marking node dead
911
972
  log.warn(tag, `❌ Node ${batchStartIndex + index + 1}/${nodes.length} failed: ${node.service.substring(0, 50)} - ${e.message}`);
912
- 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
+ }
913
993
  throw e;
914
994
  }
915
995
  });
916
- // Race this batch - return first successful result
917
- try {
918
- 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') {
919
1001
  log.info(tag, `Token metadata fetched successfully:`, {
920
- name: winner.result.name,
921
- symbol: winner.result.symbol,
922
- decimals: winner.result.decimals,
923
- 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
924
1006
  });
925
- return winner.result;
926
- }
927
- catch (e) {
928
- log.warn(tag, `Batch ${Math.floor(batchStartIndex / RACE_BATCH_SIZE) + 1} failed, trying next batch...`);
1007
+ return successResult.value.result;
929
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...`);
930
1011
  batchStartIndex += RACE_BATCH_SIZE;
931
1012
  }
932
1013
  // All batches failed
@@ -948,23 +1029,38 @@ exports.getTokenMetadataByNetwork = getTokenMetadataByNetwork;
948
1029
  */
949
1030
  const estimateGasByNetwork = async function (networkId, transaction) {
950
1031
  let tag = TAG + ' | estimateGasByNetwork | ';
951
- try {
952
- let node = NODES.find((n) => n.networkId === networkId);
953
- if (!node)
954
- throw Error(`Node not found for networkId: ${networkId}`);
955
- // Extract chain ID from networkId
956
- let chainId;
957
- if (networkId.includes(':')) {
958
- chainId = parseInt(networkId.split(':')[1]);
959
- }
960
- const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
961
- const gasEstimate = await provider.estimateGas(transaction);
962
- 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}`);
963
1036
  }
964
- catch (e) {
965
- log.error(tag, e);
966
- throw e;
1037
+ // Extract chain ID from networkId
1038
+ let chainId;
1039
+ if (networkId.includes(':')) {
1040
+ chainId = parseInt(networkId.split(':')[1]);
967
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}`);
968
1064
  };
969
1065
  exports.estimateGasByNetwork = estimateGasByNetwork;
970
1066
  /**
@@ -972,38 +1068,55 @@ exports.estimateGasByNetwork = estimateGasByNetwork;
972
1068
  */
973
1069
  const broadcastByNetwork = async function (networkId, signedTx) {
974
1070
  let tag = TAG + ' | broadcastByNetwork | ';
975
- try {
976
- let node = NODES.find((n) => n.networkId === networkId);
977
- if (!node)
978
- throw Error(`Node not found for networkId: ${networkId}`);
979
- // Extract chain ID from networkId
980
- let chainId;
981
- if (networkId.includes(':')) {
982
- chainId = parseInt(networkId.split(':')[1]);
983
- }
984
- const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
985
- const result = await provider.sendTransaction(signedTx);
986
- // Return in expected format for pioneer-server
987
- return {
988
- success: true,
989
- txid: result.hash,
990
- transactionHash: result.hash,
991
- from: result.from,
992
- to: result.to,
993
- nonce: result.nonce,
994
- gasLimit: result.gasLimit?.toString(),
995
- gasPrice: result.gasPrice?.toString(),
996
- chainId: result.chainId
997
- };
998
- }
999
- catch (e) {
1000
- log.error(tag, e);
1001
- // 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) {
1002
1074
  return {
1003
1075
  success: false,
1004
- error: e.message || e.toString()
1076
+ error: `No nodes found for networkId: ${networkId}`
1005
1077
  };
1006
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
+ };
1007
1120
  };
1008
1121
  exports.broadcastByNetwork = broadcastByNetwork;
1009
1122
  /**
@@ -1011,25 +1124,40 @@ exports.broadcastByNetwork = broadcastByNetwork;
1011
1124
  */
1012
1125
  const getTransactionByNetwork = async function (networkId, txid) {
1013
1126
  let tag = TAG + ' | getTransactionByNetwork | ';
1014
- try {
1015
- let node = NODES.find((n) => n.networkId === networkId);
1016
- if (!node)
1017
- throw Error(`Node not found for networkId: ${networkId}`);
1018
- // Extract chain ID from networkId
1019
- let chainId;
1020
- if (networkId.includes(':')) {
1021
- chainId = parseInt(networkId.split(':')[1]);
1022
- }
1023
- const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
1024
- log.info(tag, `Fetching transaction ${txid} on ${networkId}`);
1025
- const tx = await provider.getTransaction(txid);
1026
- const receipt = await provider.getTransactionReceipt(txid);
1027
- 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}`);
1028
1131
  }
1029
- catch (e) {
1030
- log.error(tag, e);
1031
- throw e;
1132
+ // Extract chain ID from networkId
1133
+ let chainId;
1134
+ if (networkId.includes(':')) {
1135
+ chainId = parseInt(networkId.split(':')[1]);
1032
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}`);
1033
1161
  };
1034
1162
  exports.getTransactionByNetwork = getTransactionByNetwork;
1035
1163
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pioneer-platform/eth-network",
3
- "version": "8.14.0",
3
+ "version": "8.14.2",
4
4
  "main": "./lib/index.js",
5
5
  "types": "./lib/index.d.ts",
6
6
  "scripts": {