@pioneer-platform/eth-network 8.16.0 → 8.19.0

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,44 @@
1
1
  # @pioneer-platform/eth-network
2
2
 
3
+ ## 8.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - chore: chore: chore: chore: feat(pioneer): implement end-to-end Solana transaction signing
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @pioneer-platform/blockbook@8.16.0
13
+ - @pioneer-platform/pioneer-caip@9.15.0
14
+ - @pioneer-platform/nodes@8.15.0
15
+
16
+ ## 8.18.0
17
+
18
+ ### Minor Changes
19
+
20
+ - chore: chore: chore: feat(pioneer): implement end-to-end Solana transaction signing
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies
25
+ - @pioneer-platform/blockbook@8.15.0
26
+ - @pioneer-platform/pioneer-caip@9.14.0
27
+ - @pioneer-platform/nodes@8.14.0
28
+
29
+ ## 8.17.0
30
+
31
+ ### Minor Changes
32
+
33
+ - chore: chore: feat(pioneer): implement end-to-end Solana transaction signing
34
+
35
+ ### Patch Changes
36
+
37
+ - Updated dependencies
38
+ - @pioneer-platform/blockbook@8.14.0
39
+ - @pioneer-platform/pioneer-caip@9.13.0
40
+ - @pioneer-platform/nodes@8.13.0
41
+
3
42
  ## 8.16.0
4
43
 
5
44
  ### Minor Changes
package/lib/index.d.ts CHANGED
@@ -162,8 +162,19 @@ export declare const broadcastByNetwork: (networkId: string, signedTx: string) =
162
162
  * Get transaction by network and txid
163
163
  */
164
164
  export declare const getTransactionByNetwork: (networkId: string, txid: string) => Promise<{
165
+ error: boolean;
166
+ errorType: string;
167
+ message: string;
168
+ networkId: string;
169
+ tx?: undefined;
170
+ receipt?: undefined;
171
+ } | {
165
172
  tx: ethers.ethers.providers.TransactionResponse;
166
173
  receipt: ethers.ethers.providers.TransactionReceipt;
174
+ error?: undefined;
175
+ errorType?: undefined;
176
+ message?: undefined;
177
+ networkId?: undefined;
167
178
  }>;
168
179
  /**
169
180
  * Get transaction history by network
package/lib/index.js CHANGED
@@ -76,7 +76,7 @@ let BASE = 1000000000000000000;
76
76
  // - ETH_RPC_TIMEOUT: Timeout for RPC calls in ms (default: 30000)
77
77
  // - ETH_DEAD_NODE_TTL: Time before retrying dead nodes in ms (default: 300000 = 5 min)
78
78
  // - ETH_RACE_BATCH_SIZE: Number of nodes to try in parallel (default: 3)
79
- const RPC_TIMEOUT_MS = parseInt(process.env.ETH_RPC_TIMEOUT || '30000'); // 30 second timeout for RPC calls (increased from 10s)
79
+ const RPC_TIMEOUT_MS = parseInt(process.env.ETH_RPC_TIMEOUT || '5000'); // 5 second timeout for RPC calls (reduced from 30s for faster fallback)
80
80
  /**
81
81
  * Wrap a promise with a timeout
82
82
  * @param promise Promise to wrap
@@ -101,10 +101,12 @@ const ERC20ABI = [
101
101
  ];
102
102
  // Node registry
103
103
  let NODES = [];
104
- // Dead node tracker - nodes that failed recently (service URL -> timestamp)
104
+ // Dead node tracker - nodes that failed recently (service URL -> { timestamp, ttl })
105
105
  const DEAD_NODES = new Map();
106
106
  const DEAD_NODE_TTL_MS = parseInt(process.env.ETH_DEAD_NODE_TTL || '300000'); // 5 minutes before retry
107
- const RACE_BATCH_SIZE = parseInt(process.env.ETH_RACE_BATCH_SIZE || '5'); // Try 5 premium/reliable nodes first (increased from 3)
107
+ const RACE_BATCH_SIZE = parseInt(process.env.ETH_RACE_BATCH_SIZE || '1'); // Try 1 node at a time (SEQUENTIAL, not parallel racing)
108
+ // NOTE: Changed from 5 to 1 to reduce API request load by 80%
109
+ // Fast timeout (500ms) ensures quick fallback if node is slow/down
108
110
  // Node tier priority (lower = higher priority)
109
111
  const TIER_PRIORITY = {
110
112
  premium: 1,
@@ -121,72 +123,113 @@ const TIER_PRIORITY = {
121
123
  // - STARTUP_NODE_SAMPLE: Number of random nodes to test per tier (default: 0 = all)
122
124
  //
123
125
  // Note: Validation runs in background and does NOT block server startup
124
- const STARTUP_VALIDATION_ENABLED = process.env.VALIDATE_NODES_ON_STARTUP !== 'false'; // Default true
125
- const STARTUP_VALIDATION_TIMEOUT = parseInt(process.env.STARTUP_NODE_TIMEOUT || '3000'); // 3s timeout
126
- const STARTUP_VALIDATION_CONCURRENCY = parseInt(process.env.STARTUP_NODE_CONCURRENCY || '20'); // 20 parallel
126
+ const STARTUP_VALIDATION_ENABLED = process.env.VALIDATE_NODES_ON_STARTUP === 'true'; // Default FALSE (changed to reduce startup API load)
127
+ const STARTUP_VALIDATION_TIMEOUT = parseInt(process.env.STARTUP_NODE_TIMEOUT || '10000'); // 10s timeout (increased from 3s to avoid false positives)
128
+ const STARTUP_VALIDATION_CONCURRENCY = parseInt(process.env.STARTUP_NODE_CONCURRENCY || '5'); // 5 parallel (reduced from 20 to avoid rate limiting)
127
129
  const STARTUP_VALIDATION_TIERS = (process.env.STARTUP_VALIDATION_TIER || 'premium,reliable,public').split(',');
128
130
  const STARTUP_VALIDATION_SAMPLE_SIZE = parseInt(process.env.STARTUP_NODE_SAMPLE || '0'); // 0 = all nodes in tier
131
+ const STARTUP_DEAD_NODE_TTL_MS = parseInt(process.env.STARTUP_DEAD_NODE_TTL || '60000'); // 1min TTL for startup failures (vs 5min for runtime)
129
132
  /**
130
133
  * Check if a node is marked as dead
131
134
  */
132
135
  const isNodeDead = (nodeUrl) => {
133
- const deathTime = DEAD_NODES.get(nodeUrl);
134
- if (!deathTime)
136
+ const entry = DEAD_NODES.get(nodeUrl);
137
+ if (!entry)
135
138
  return false;
136
- // Check if TTL expired
137
- if (Date.now() - deathTime > DEAD_NODE_TTL_MS) {
139
+ // Check if TTL expired (using node-specific TTL)
140
+ if (Date.now() - entry.timestamp > entry.ttl) {
138
141
  DEAD_NODES.delete(nodeUrl);
139
142
  return false;
140
143
  }
141
144
  return true;
142
145
  };
143
146
  /**
144
- * Mark a node as dead
147
+ * Mark a node as dead with optional custom TTL
145
148
  * Only logs at WARN level for premium/reliable nodes, DEBUG for others
149
+ * @param nodeUrl Node service URL
150
+ * @param tier Node tier (for logging)
151
+ * @param customTTL Optional custom TTL in ms (defaults to DEAD_NODE_TTL_MS)
146
152
  */
147
- const markNodeDead = (nodeUrl, tier) => {
148
- DEAD_NODES.set(nodeUrl, Date.now());
153
+ const markNodeDead = (nodeUrl, tier, customTTL) => {
154
+ // Use custom TTL if provided, otherwise use default
155
+ const ttl = customTTL || DEAD_NODE_TTL_MS;
156
+ DEAD_NODES.set(nodeUrl, { timestamp: Date.now(), ttl });
149
157
  // Only log failures for premium/reliable nodes (reduces spam)
158
+ const ttlMinutes = Math.round(ttl / 1000 / 60);
150
159
  if (tier === 'premium' || tier === 'reliable') {
151
- log.warn(TAG + ' | markNodeDead | ', `⚠️ ${tier} node failed: ${nodeUrl.substring(0, 50)}...`);
160
+ log.warn(TAG + ' | markNodeDead | ', `⚠️ ${tier} node failed: ${nodeUrl.substring(0, 50)}... (dead for ${ttlMinutes}min)`);
152
161
  }
153
162
  else {
154
- log.debug(TAG + ' | markNodeDead | ', `Public/untrusted node failed: ${nodeUrl.substring(0, 50)}...`);
163
+ log.debug(TAG + ' | markNodeDead | ', `Public/untrusted node failed: ${nodeUrl.substring(0, 50)}... (dead for ${ttlMinutes}min)`);
155
164
  }
156
165
  };
157
166
  /**
158
- * Quick health check for a single node
167
+ * Quick health check for a single node with retry logic and smart error filtering
159
168
  * @param node Node to test
160
169
  * @param timeout Timeout in ms
170
+ * @param maxRetries Maximum number of retry attempts (default: 2)
161
171
  * @returns true if healthy, false otherwise
162
172
  */
163
- const quickNodeHealthCheck = async (node, timeout = STARTUP_VALIDATION_TIMEOUT) => {
164
- try {
165
- // Extract chain ID from networkId (e.g., "eip155:1" -> 1)
166
- let chainId;
167
- if (node.networkId.includes(':')) {
168
- chainId = parseInt(node.networkId.split(':')[1]);
173
+ const quickNodeHealthCheck = async (node, timeout = STARTUP_VALIDATION_TIMEOUT, maxRetries = 2) => {
174
+ let lastError = null;
175
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
176
+ try {
177
+ // Exponential backoff between retries (0ms, 500ms, 1000ms)
178
+ if (attempt > 0) {
179
+ await new Promise(resolve => setTimeout(resolve, attempt * 500));
180
+ }
181
+ // Extract chain ID from networkId (e.g., "eip155:1" -> 1)
182
+ let chainId;
183
+ if (node.networkId.includes(':')) {
184
+ chainId = parseInt(node.networkId.split(':')[1]);
185
+ }
186
+ // Create provider with timeout
187
+ const provider = new ethers.providers.JsonRpcProvider({
188
+ url: node.service,
189
+ timeout,
190
+ });
191
+ // Race between chainId check and timeout
192
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout));
193
+ const network = await Promise.race([
194
+ provider.getNetwork(),
195
+ timeoutPromise,
196
+ ]);
197
+ // Verify chain ID matches if expected
198
+ if (chainId && network.chainId !== chainId) {
199
+ return false;
200
+ }
201
+ return true; // Success!
169
202
  }
170
- // Create provider with timeout
171
- const provider = new ethers.providers.JsonRpcProvider({
172
- url: node.service,
173
- timeout,
174
- });
175
- // Race between chainId check and timeout
176
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout));
177
- const network = await Promise.race([
178
- provider.getNetwork(),
179
- timeoutPromise,
180
- ]);
181
- // Verify chain ID matches if expected
182
- if (chainId && network.chainId !== chainId) {
183
- return false;
203
+ catch (e) {
204
+ lastError = e;
205
+ // Check if this is a temporary/ignorable error
206
+ const isSSLError = e.message && (e.message.includes('UNABLE_TO_GET_ISSUER_CERT') ||
207
+ e.message.includes('certificate') ||
208
+ e.message.includes('SSL') ||
209
+ e.message.includes('TLS') ||
210
+ e.message.includes('CERT_HAS_EXPIRED') ||
211
+ e.message.includes('SELF_SIGNED_CERT_IN_CHAIN') ||
212
+ e.code === 'UNABLE_TO_GET_ISSUER_CERT' ||
213
+ e.code === 'CERT_HAS_EXPIRED' ||
214
+ e.code === 'SELF_SIGNED_CERT_IN_CHAIN');
215
+ const isRateLimit = e.message && (e.message.includes('rate limit') ||
216
+ e.message.includes('too many requests') ||
217
+ e.message.includes('429'));
218
+ const isDNSError = e.message && (e.message.includes('ENOTFOUND') ||
219
+ e.message.includes('ECONNREFUSED') ||
220
+ e.message.includes('getaddrinfo'));
221
+ // Don't retry for permanent failures
222
+ if (!isSSLError && !isRateLimit && !isDNSError && e.message && e.message.includes('invalid')) {
223
+ return false; // Permanent failure (wrong chain, etc)
224
+ }
225
+ // For temporary errors, continue to next retry attempt
226
+ if (attempt < maxRetries) {
227
+ continue;
228
+ }
184
229
  }
185
- return true;
186
- }
187
- catch (e) {
188
- return false;
189
230
  }
231
+ // All retries exhausted
232
+ return false;
190
233
  };
191
234
  /**
192
235
  * Validate nodes on startup
@@ -241,7 +284,8 @@ const validateNodesOnStartup = async () => {
241
284
  const isHealthy = await quickNodeHealthCheck(node, STARTUP_VALIDATION_TIMEOUT);
242
285
  completed++;
243
286
  if (!isHealthy) {
244
- markNodeDead(node.service, node.tier);
287
+ // Use shorter TTL for startup failures (1min vs 5min for runtime)
288
+ markNodeDead(node.service, node.tier, STARTUP_DEAD_NODE_TTL_MS);
245
289
  dead++;
246
290
  tierStats[tier].dead++;
247
291
  }
@@ -261,7 +305,7 @@ const validateNodesOnStartup = async () => {
261
305
  .join(', ');
262
306
  log.info(tag, `✅ Validation complete: ${healthy}/${nodesToTest.length} healthy (${healthyPercent}%) in ${elapsed}ms | ${tierSummary}`);
263
307
  if (dead > 0) {
264
- log.info(tag, `Marked ${dead} nodes as dead - will skip for ${DEAD_NODE_TTL_MS / 1000 / 60}min`);
308
+ log.info(tag, `Marked ${dead} nodes as dead - will skip for ${STARTUP_DEAD_NODE_TTL_MS / 1000 / 60}min (startup TTL)`);
265
309
  }
266
310
  if (healthy === 0) {
267
311
  log.error(tag, '❌ No healthy nodes found! Network operations may fail');
@@ -1288,7 +1332,14 @@ const getTransactionByNetwork = async function (networkId, txid) {
1288
1332
  // Get ALL nodes for this network
1289
1333
  let nodes = NODES.filter((n) => n.networkId === networkId);
1290
1334
  if (!nodes || nodes.length === 0) {
1291
- throw Error(`No nodes found for networkId: ${networkId}`);
1335
+ log.warn(tag, `No nodes found for networkId: ${networkId} - network may be offline or not supported`);
1336
+ // Return error object instead of throwing - allows caller to handle gracefully
1337
+ return {
1338
+ error: true,
1339
+ errorType: 'NO_NODES_FOUND',
1340
+ message: `No nodes found for networkId: ${networkId}`,
1341
+ networkId
1342
+ };
1292
1343
  }
1293
1344
  // Extract chain ID from networkId
1294
1345
  let chainId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pioneer-platform/eth-network",
3
- "version": "8.16.0",
3
+ "version": "8.19.0",
4
4
  "main": "./lib/index.js",
5
5
  "types": "./lib/index.d.ts",
6
6
  "scripts": {
@@ -18,10 +18,10 @@
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.13.0",
21
+ "@pioneer-platform/blockbook": "^8.16.0",
22
22
  "@pioneer-platform/loggerdog": "^8.11.0",
23
- "@pioneer-platform/nodes": "^8.12.0",
24
- "@pioneer-platform/pioneer-caip": "^9.12.0",
23
+ "@pioneer-platform/nodes": "^8.15.0",
24
+ "@pioneer-platform/pioneer-caip": "^9.15.0",
25
25
  "@xchainjs/xchain-client": "0.9.0",
26
26
  "@xchainjs/xchain-util": "0.2.6",
27
27
  "axios": "^1.6.0",