@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 +39 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +94 -43
- package/package.json +4 -4
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 || '
|
|
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 || '
|
|
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
|
|
125
|
-
const STARTUP_VALIDATION_TIMEOUT = parseInt(process.env.STARTUP_NODE_TIMEOUT || '
|
|
126
|
-
const STARTUP_VALIDATION_CONCURRENCY = parseInt(process.env.STARTUP_NODE_CONCURRENCY || '
|
|
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
|
|
134
|
-
if (!
|
|
136
|
+
const entry = DEAD_NODES.get(nodeUrl);
|
|
137
|
+
if (!entry)
|
|
135
138
|
return false;
|
|
136
|
-
// Check if TTL expired
|
|
137
|
-
if (Date.now() -
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
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.
|
|
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.
|
|
21
|
+
"@pioneer-platform/blockbook": "^8.16.0",
|
|
22
22
|
"@pioneer-platform/loggerdog": "^8.11.0",
|
|
23
|
-
"@pioneer-platform/nodes": "^8.
|
|
24
|
-
"@pioneer-platform/pioneer-caip": "^9.
|
|
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",
|