@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.
- package/CHANGELOG.md +12 -0
- package/lib/index.js +255 -127
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
917
|
-
|
|
918
|
-
|
|
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:
|
|
921
|
-
symbol:
|
|
922
|
-
decimals:
|
|
923
|
-
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
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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:
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
/**
|