@pioneer-platform/eth-network 8.14.2 → 8.14.4

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @pioneer-platform/eth-network
2
2
 
3
+ ## 8.14.4
4
+
5
+ ### Patch Changes
6
+
7
+ - chore: 🔒 CRITICAL FIX: XRP destination tag validation
8
+ - Updated dependencies
9
+ - @pioneer-platform/blockbook@8.12.2
10
+
11
+ ## 8.14.3
12
+
13
+ ### Patch Changes
14
+
15
+ - fix: Add node racing to all eth-network \*ByNetwork functions
16
+
3
17
  ## 8.14.2
4
18
 
5
19
  ### Patch Changes
package/lib/index.d.ts CHANGED
@@ -57,8 +57,14 @@ export declare const getBalances: (addresses: string[]) => Promise<string[]>;
57
57
  export declare const getBalanceToken: (address: string, tokenAddress: string) => Promise<string>;
58
58
  /**
59
59
  * Get ERC20 allowance
60
+ * @deprecated Use getAllowanceByNetwork instead
60
61
  */
61
62
  export declare const getAllowance: (tokenAddress: string, spender: string, sender: string) => Promise<any>;
63
+ /**
64
+ * Get ERC20 allowance by network with node racing
65
+ * Uses the node racing system for reliability and performance
66
+ */
67
+ export declare const getAllowanceByNetwork: (networkId: string, tokenAddress: string, ownerAddress: string, spenderAddress: string) => Promise<string>;
62
68
  /**
63
69
  * Get symbol from contract address
64
70
  */
@@ -117,8 +123,20 @@ export declare const estimateGasByNetwork: (networkId: string, transaction: any)
117
123
  */
118
124
  export declare const broadcastByNetwork: (networkId: string, signedTx: string) => Promise<{
119
125
  success: boolean;
120
- txid: string;
121
- transactionHash: string;
126
+ error: string;
127
+ txid?: undefined;
128
+ transactionHash?: undefined;
129
+ from?: undefined;
130
+ to?: undefined;
131
+ nonce?: undefined;
132
+ gasLimit?: undefined;
133
+ gasPrice?: undefined;
134
+ chainId?: undefined;
135
+ details?: undefined;
136
+ } | {
137
+ success: boolean;
138
+ txid: any;
139
+ transactionHash: any;
122
140
  from: string;
123
141
  to: string;
124
142
  nonce: number;
@@ -126,9 +144,11 @@ export declare const broadcastByNetwork: (networkId: string, signedTx: string) =
126
144
  gasPrice: string;
127
145
  chainId: number;
128
146
  error?: undefined;
147
+ details?: undefined;
129
148
  } | {
130
149
  success: boolean;
131
150
  error: any;
151
+ details: string;
132
152
  txid?: undefined;
133
153
  transactionHash?: undefined;
134
154
  from?: undefined;
package/lib/index.js CHANGED
@@ -43,7 +43,7 @@ var __importStar = (this && this.__importStar) || (function () {
43
43
  };
44
44
  })();
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
- exports.getFees = exports.getBalanceAddress = exports.getTransactionsByNetwork = exports.getTransactionByNetwork = exports.broadcastByNetwork = exports.estimateGasByNetwork = exports.getTokenMetadataByNetwork = exports.getTokenMetadata = exports.getTokenDecimalsByNetwork = exports.getBalanceTokensByNetwork = exports.getBalanceTokenByNetwork = exports.getBalanceAddressByNetwork = exports.getMemoEncoded = exports.getTransferData = exports.broadcast = exports.getTransaction = exports.getSymbolFromContract = exports.getAllowance = exports.getBalanceToken = exports.getBalances = exports.getBalance = exports.estimateFee = exports.getGasPriceByNetwork = exports.getGasPrice = exports.getNonceByNetwork = exports.getNonce = exports.addNodes = exports.addNode = exports.getNodes = exports.init = void 0;
46
+ exports.getFees = exports.getBalanceAddress = exports.getTransactionsByNetwork = exports.getTransactionByNetwork = exports.broadcastByNetwork = exports.estimateGasByNetwork = exports.getTokenMetadataByNetwork = exports.getTokenMetadata = exports.getTokenDecimalsByNetwork = exports.getBalanceTokensByNetwork = exports.getBalanceTokenByNetwork = exports.getBalanceAddressByNetwork = exports.getMemoEncoded = exports.getTransferData = exports.broadcast = exports.getTransaction = exports.getSymbolFromContract = exports.getAllowanceByNetwork = exports.getAllowance = exports.getBalanceToken = exports.getBalances = exports.getBalance = exports.estimateFee = exports.getGasPriceByNetwork = exports.getGasPrice = exports.getNonceByNetwork = exports.getNonce = exports.addNodes = exports.addNode = exports.getNodes = exports.init = void 0;
47
47
  const TAG = " | eth-network | ";
48
48
  const ethers = __importStar(require("ethers"));
49
49
  const BigNumber = require('bignumber.js');
@@ -553,6 +553,7 @@ const getBalanceToken = async function (address, tokenAddress) {
553
553
  exports.getBalanceToken = getBalanceToken;
554
554
  /**
555
555
  * Get ERC20 allowance
556
+ * @deprecated Use getAllowanceByNetwork instead
556
557
  */
557
558
  const getAllowance = async function (tokenAddress, spender, sender) {
558
559
  let tag = TAG + ' | getAllowance | ';
@@ -567,6 +568,70 @@ const getAllowance = async function (tokenAddress, spender, sender) {
567
568
  }
568
569
  };
569
570
  exports.getAllowance = getAllowance;
571
+ /**
572
+ * Get ERC20 allowance by network with node racing
573
+ * Uses the node racing system for reliability and performance
574
+ */
575
+ const getAllowanceByNetwork = async function (networkId, tokenAddress, ownerAddress, spenderAddress) {
576
+ let tag = TAG + ' | getAllowanceByNetwork | ';
577
+ // Get ALL nodes for this network, excluding dead ones
578
+ let allNodes = NODES.filter((n) => n.networkId === networkId);
579
+ if (!allNodes || allNodes.length === 0) {
580
+ throw Error(`No nodes found for networkId: ${networkId}`);
581
+ }
582
+ // Filter out dead nodes and sort by tier priority
583
+ let nodes = allNodes
584
+ .filter((n) => !isNodeDead(n.service))
585
+ .sort((a, b) => {
586
+ const tierA = a.tier || 'public';
587
+ const tierB = b.tier || 'public';
588
+ return TIER_PRIORITY[tierA] - TIER_PRIORITY[tierB];
589
+ });
590
+ if (nodes.length === 0) {
591
+ log.error(tag, `All ${allNodes.length} nodes are marked dead for ${networkId} token ${tokenAddress} - failing fast`);
592
+ throw Error(`No healthy nodes available for ${networkId} token ${tokenAddress} (all ${allNodes.length} nodes are dead)`);
593
+ }
594
+ const topTier = nodes[0].tier || 'public';
595
+ log.info(tag, `Racing top ${Math.min(RACE_BATCH_SIZE, nodes.length)} nodes (${nodes.length} alive of ${allNodes.length} total, starting with ${topTier})`);
596
+ // Extract chain ID from networkId (e.g., "eip155:1" -> 1)
597
+ let chainId;
598
+ if (networkId.includes(':')) {
599
+ chainId = parseInt(networkId.split(':')[1]);
600
+ }
601
+ // Race nodes in batches
602
+ let batchStartIndex = 0;
603
+ while (batchStartIndex < nodes.length) {
604
+ const batch = nodes.slice(batchStartIndex, batchStartIndex + RACE_BATCH_SIZE);
605
+ // Create racing promises for this batch
606
+ const racePromises = batch.map(async (node, index) => {
607
+ try {
608
+ const provider = new ethers.providers.StaticJsonRpcProvider(node.service, chainId ? { chainId, name: node.networkId } : undefined);
609
+ const contract = new ethers.Contract(tokenAddress, ERC20ABI, provider);
610
+ // Wrap contract call with timeout
611
+ const allowance = await withTimeout(contract.allowance(ownerAddress, spenderAddress), RPC_TIMEOUT_MS, `Node ${node.service} allowance timeout`);
612
+ log.info(tag, `Node ${node.service} (tier: ${node.tier || 'public'}) returned allowance: ${allowance.toString()}`);
613
+ return allowance.toString();
614
+ }
615
+ catch (error) {
616
+ log.error(tag, `Node ${node.service} failed:`, error.message);
617
+ throw error;
618
+ }
619
+ });
620
+ try {
621
+ // Race this batch of nodes
622
+ const result = await Promise.race(racePromises);
623
+ log.info(tag, `Successfully got allowance from racing batch starting at index ${batchStartIndex}`);
624
+ return result;
625
+ }
626
+ catch (error) {
627
+ log.warn(tag, `Batch starting at ${batchStartIndex} failed, trying next batch if available`);
628
+ }
629
+ batchStartIndex += RACE_BATCH_SIZE;
630
+ }
631
+ // If we get here, all batches failed
632
+ throw Error(`All ${nodes.length} healthy nodes failed for ${networkId} token ${tokenAddress} allowance check`);
633
+ };
634
+ exports.getAllowanceByNetwork = getAllowanceByNetwork;
570
635
  /**
571
636
  * Get symbol from contract address
572
637
  */
@@ -1081,41 +1146,133 @@ const broadcastByNetwork = async function (networkId, signedTx) {
1081
1146
  if (networkId.includes(':')) {
1082
1147
  chainId = parseInt(networkId.split(':')[1]);
1083
1148
  }
1149
+ // VALIDATE TRANSACTION BEFORE BROADCASTING
1150
+ // This catches issues early before attempting RPC calls
1151
+ try {
1152
+ log.info(tag, 'Validating signed transaction before broadcast...');
1153
+ const parsedTx = ethers.utils.parseTransaction(signedTx);
1154
+ log.info(tag, 'Parsed transaction fields:', {
1155
+ to: parsedTx.to,
1156
+ from: parsedTx.from,
1157
+ nonce: parsedTx.nonce,
1158
+ gasLimit: parsedTx.gasLimit?.toString(),
1159
+ gasPrice: parsedTx.gasPrice?.toString(),
1160
+ chainId: parsedTx.chainId,
1161
+ value: parsedTx.value?.toString(),
1162
+ data: parsedTx.data?.substring(0, 20) + '...'
1163
+ });
1164
+ // CRITICAL VALIDATION: Check for gasLimit = 0
1165
+ if (!parsedTx.gasLimit || parsedTx.gasLimit.isZero()) {
1166
+ const errorMsg = `Transaction validation failed: gasLimit is 0 or missing. This transaction will be rejected by the network. gasLimit=${parsedTx.gasLimit?.toString()}`;
1167
+ log.error(tag, errorMsg);
1168
+ return {
1169
+ success: false,
1170
+ error: errorMsg
1171
+ };
1172
+ }
1173
+ // CRITICAL VALIDATION: Check for gasPrice = 0 (for non-EIP1559 txs)
1174
+ if (!parsedTx.gasPrice || parsedTx.gasPrice.isZero()) {
1175
+ const errorMsg = `Transaction validation failed: gasPrice is 0 or missing. This transaction will be rejected by the network. gasPrice=${parsedTx.gasPrice?.toString()}`;
1176
+ log.error(tag, errorMsg);
1177
+ return {
1178
+ success: false,
1179
+ error: errorMsg
1180
+ };
1181
+ }
1182
+ // Validate chainId matches network
1183
+ if (chainId && parsedTx.chainId !== chainId) {
1184
+ const errorMsg = `Transaction validation failed: chainId mismatch. Expected ${chainId}, got ${parsedTx.chainId}`;
1185
+ log.error(tag, errorMsg);
1186
+ return {
1187
+ success: false,
1188
+ error: errorMsg
1189
+ };
1190
+ }
1191
+ log.info(tag, '✅ Transaction validation passed - proceeding with broadcast');
1192
+ }
1193
+ catch (validationError) {
1194
+ log.error(tag, 'Transaction parsing/validation failed:', validationError);
1195
+ return {
1196
+ success: false,
1197
+ error: `Invalid transaction format: ${validationError.message}`
1198
+ };
1199
+ }
1084
1200
  // Try each node until one succeeds
1085
1201
  let lastError = null;
1202
+ let allErrors = [];
1086
1203
  for (let i = 0; i < nodes.length; i++) {
1087
1204
  const node = nodes[i];
1088
1205
  try {
1089
1206
  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)}`);
1207
+ // Use raw JSON-RPC call for immediate validation errors
1208
+ // eth_sendRawTransaction will validate the tx BEFORE returning
1209
+ log.info(tag, `Broadcasting via eth_sendRawTransaction to ${node.service.substring(0, 50)}...`);
1210
+ const txHash = await withTimeout(provider.send('eth_sendRawTransaction', [signedTx]), RPC_TIMEOUT_MS, `Broadcast timeout after ${RPC_TIMEOUT_MS}ms for ${node.service.substring(0, 50)}`);
1211
+ // If we got here, the transaction was accepted by the network
1212
+ log.info(tag, `✅ Transaction accepted by network: ${txHash}`);
1091
1213
  // Success! Log which node worked (only if not the first one)
1092
1214
  if (i > 0) {
1093
1215
  log.info(tag, `Successfully broadcasted from node ${i + 1}/${nodes.length}: ${node.service.substring(0, 50)}`);
1094
1216
  }
1217
+ // Parse the original transaction to get details for response
1218
+ const parsedTx = ethers.utils.parseTransaction(signedTx);
1095
1219
  // Return in expected format for pioneer-server
1096
1220
  return {
1097
1221
  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
1222
+ txid: txHash,
1223
+ transactionHash: txHash,
1224
+ from: parsedTx.from,
1225
+ to: parsedTx.to,
1226
+ nonce: parsedTx.nonce,
1227
+ gasLimit: parsedTx.gasLimit?.toString(),
1228
+ gasPrice: parsedTx.gasPrice?.toString(),
1229
+ chainId: parsedTx.chainId
1106
1230
  };
1107
1231
  }
1108
1232
  catch (e) {
1109
1233
  lastError = e;
1110
- log.warn(tag, `Node ${i + 1}/${nodes.length} failed (${node.service.substring(0, 50)}): ${e.message}`);
1234
+ // Extract detailed error message from RPC response
1235
+ let errorMessage = e.message || 'Unknown error';
1236
+ // Check for JSON-RPC error format
1237
+ if (e.error) {
1238
+ if (typeof e.error === 'string') {
1239
+ errorMessage = e.error;
1240
+ }
1241
+ else if (e.error.message) {
1242
+ errorMessage = e.error.message;
1243
+ }
1244
+ }
1245
+ else if (e.body) {
1246
+ try {
1247
+ const body = JSON.parse(e.body);
1248
+ if (body.error) {
1249
+ if (typeof body.error === 'string') {
1250
+ errorMessage = body.error;
1251
+ }
1252
+ else if (body.error.message) {
1253
+ errorMessage = body.error.message;
1254
+ }
1255
+ }
1256
+ }
1257
+ catch (parseErr) {
1258
+ // Ignore parsing errors
1259
+ }
1260
+ }
1261
+ // Log the detailed error
1262
+ log.error(tag, `❌ Broadcast rejected by node ${i + 1}: ${errorMessage}`);
1263
+ allErrors.push(`Node ${i + 1}: ${errorMessage}`);
1264
+ log.warn(tag, `Node ${i + 1}/${nodes.length} failed (${node.service.substring(0, 50)}): ${errorMessage}`);
1111
1265
  // Continue to next node
1112
1266
  }
1113
1267
  }
1114
- // All nodes failed
1268
+ // All nodes failed - provide comprehensive error details
1269
+ const detailedError = allErrors.join(' | ');
1115
1270
  log.error(tag, `All ${nodes.length} nodes failed for ${networkId}`);
1271
+ log.error(tag, 'Detailed errors:', detailedError);
1116
1272
  return {
1117
1273
  success: false,
1118
- error: lastError?.message || `Failed to broadcast from any node for ${networkId}`
1274
+ error: lastError?.message || `Failed to broadcast from any node for ${networkId}`,
1275
+ details: detailedError
1119
1276
  };
1120
1277
  };
1121
1278
  exports.broadcastByNetwork = broadcastByNetwork;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pioneer-platform/eth-network",
3
- "version": "8.14.2",
3
+ "version": "8.14.4",
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.12.0",
21
+ "@pioneer-platform/blockbook": "^8.12.2",
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",