@dignetwork/dig-sdk 0.0.1-alpha.14 → 0.0.1-alpha.141
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/dist/DataIntegrityTree/DataIntegrityTree.d.ts +11 -2
- package/dist/DataIntegrityTree/DataIntegrityTree.d.ts.map +1 -1
- package/dist/DataIntegrityTree/DataIntegrityTree.js +106 -15
- package/dist/DigNetwork/ContentServer.d.ts +12 -5
- package/dist/DigNetwork/ContentServer.d.ts.map +1 -1
- package/dist/DigNetwork/ContentServer.js +172 -24
- package/dist/DigNetwork/DigNetwork.d.ts +6 -9
- package/dist/DigNetwork/DigNetwork.d.ts.map +1 -1
- package/dist/DigNetwork/DigNetwork.js +137 -189
- package/dist/DigNetwork/DigPeer.d.ts +8 -3
- package/dist/DigNetwork/DigPeer.d.ts.map +1 -1
- package/dist/DigNetwork/DigPeer.js +68 -115
- package/dist/DigNetwork/IncentiveServer.d.ts.map +1 -1
- package/dist/DigNetwork/IncentiveServer.js +6 -5
- package/dist/DigNetwork/PropagationServer.d.ts +83 -33
- package/dist/DigNetwork/PropagationServer.d.ts.map +1 -1
- package/dist/DigNetwork/PropagationServer.js +605 -356
- package/dist/blockchain/DataStore.d.ts +25 -7
- package/dist/blockchain/DataStore.d.ts.map +1 -1
- package/dist/blockchain/DataStore.js +114 -159
- package/dist/blockchain/DataStoreSerializer.d.ts +1 -1
- package/dist/blockchain/DataStoreSerializer.d.ts.map +1 -1
- package/dist/blockchain/FullNodePeer.d.ts +120 -18
- package/dist/blockchain/FullNodePeer.d.ts.map +1 -1
- package/dist/blockchain/FullNodePeer.js +441 -146
- package/dist/blockchain/ServerCoin.d.ts +13 -5
- package/dist/blockchain/ServerCoin.d.ts.map +1 -1
- package/dist/blockchain/ServerCoin.js +90 -28
- package/dist/blockchain/StoreMonitorRegistry.d.ts +85 -0
- package/dist/blockchain/StoreMonitorRegistry.d.ts.map +1 -0
- package/dist/blockchain/StoreMonitorRegistry.js +242 -0
- package/dist/blockchain/Wallet.d.ts +2 -2
- package/dist/blockchain/Wallet.d.ts.map +1 -1
- package/dist/blockchain/Wallet.js +50 -18
- package/dist/blockchain/coins.d.ts +1 -2
- package/dist/blockchain/coins.d.ts.map +1 -1
- package/dist/blockchain/coins.js +1 -55
- package/dist/blockchain/index.d.ts +1 -0
- package/dist/blockchain/index.d.ts.map +1 -1
- package/dist/blockchain/index.js +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/ContentScanner.d.ts +63 -0
- package/dist/utils/ContentScanner.d.ts.map +1 -0
- package/dist/utils/ContentScanner.js +175 -0
- package/dist/utils/Environment.d.ts +21 -0
- package/dist/utils/Environment.d.ts.map +1 -0
- package/dist/utils/Environment.js +92 -0
- package/dist/utils/FileCache.d.ts +1 -1
- package/dist/utils/FileCache.d.ts.map +1 -1
- package/dist/utils/FileCache.js +2 -2
- package/dist/utils/FileTransfer.d.ts +47 -0
- package/dist/utils/FileTransfer.d.ts.map +1 -0
- package/dist/utils/FileTransfer.js +209 -0
- package/dist/utils/NconfManager.js +2 -1
- package/dist/utils/StoreArchiveManager.d.ts +45 -0
- package/dist/utils/StoreArchiveManager.d.ts.map +1 -0
- package/dist/utils/StoreArchiveManager.js +153 -0
- package/dist/utils/config.d.ts +4 -3
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +12 -7
- package/dist/utils/directoryUtils.d.ts.map +1 -1
- package/dist/utils/directoryUtils.js +9 -8
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/merkle.d.ts +2 -0
- package/dist/utils/merkle.d.ts.map +1 -0
- package/dist/utils/merkle.js +28 -0
- package/dist/utils/network.d.ts +1 -0
- package/dist/utils/network.d.ts.map +1 -1
- package/dist/utils/network.js +38 -11
- package/dist/utils/promiseUtils.d.ts +17 -0
- package/dist/utils/promiseUtils.d.ts.map +1 -0
- package/dist/utils/promiseUtils.js +38 -0
- package/dist/utils/ssl.js +2 -2
- package/package.json +17 -5
- package/dist/utils/deltaUtils.d.ts +0 -2
- package/dist/utils/deltaUtils.d.ts.map +0 -1
- package/dist/utils/deltaUtils.js +0 -83
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// FullNodePeer.ts
|
|
2
3
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
5
|
};
|
|
@@ -7,91 +8,263 @@ exports.FullNodePeer = void 0;
|
|
|
7
8
|
const path_1 = __importDefault(require("path"));
|
|
8
9
|
const os_1 = __importDefault(require("os"));
|
|
9
10
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
-
const datalayer_driver_1 = require("datalayer-driver");
|
|
11
|
-
const chia_server_coin_1 = require("chia-server-coin");
|
|
11
|
+
const datalayer_driver_1 = require("@dignetwork/datalayer-driver");
|
|
12
12
|
const promises_1 = require("dns/promises");
|
|
13
13
|
const net_1 = __importDefault(require("net"));
|
|
14
|
-
const lodash_1 = require("lodash");
|
|
15
14
|
const nanospinner_1 = require("nanospinner");
|
|
16
15
|
const config_1 = require("../utils/config");
|
|
16
|
+
const Environment_1 = require("../utils/Environment");
|
|
17
|
+
const node_cache_1 = __importDefault(require("node-cache"));
|
|
18
|
+
const bottleneck_1 = __importDefault(require("bottleneck"));
|
|
19
|
+
/**
|
|
20
|
+
* Constants defining configuration parameters.
|
|
21
|
+
*/
|
|
17
22
|
const FULLNODE_PORT = 8444;
|
|
18
23
|
const LOCALHOST = "127.0.0.1";
|
|
24
|
+
const CHIA_NODES_HOST = "chia-nodes";
|
|
19
25
|
const DNS_HOSTS = [
|
|
20
26
|
"dns-introducer.chia.net",
|
|
21
27
|
"chia.ctrlaltdel.ch",
|
|
22
28
|
"seeder.dexie.space",
|
|
23
29
|
"chia.hoffmang.com",
|
|
24
30
|
];
|
|
25
|
-
const CONNECTION_TIMEOUT = 2000;
|
|
26
|
-
const CACHE_DURATION = 30000; //
|
|
31
|
+
const CONNECTION_TIMEOUT = 2000; // in milliseconds
|
|
32
|
+
const CACHE_DURATION = 30000; // in milliseconds
|
|
33
|
+
const COOLDOWN_DURATION = 300000; // 5 minutes in milliseconds
|
|
34
|
+
const MAX_PEERS_TO_FETCH = 5; // Maximum number of peers to fetch from DNS
|
|
35
|
+
const MAX_RETRIES = 3; // Maximum number of retry attempts
|
|
36
|
+
const MAX_REQUESTS_PER_MINUTE = 100; // Per-peer rate limit
|
|
37
|
+
/**
|
|
38
|
+
* Creates a proxy for the Peer instance to handle errors, retries, and rate limiting.
|
|
39
|
+
* @param peer - The original Peer instance.
|
|
40
|
+
* @param peerIP - The IP address of the peer.
|
|
41
|
+
* @param retryCount - The current retry attempt.
|
|
42
|
+
* @returns {Peer} - The proxied Peer instance.
|
|
43
|
+
*/
|
|
44
|
+
function createPeerProxy(peer, peerIP, retryCount = 0) {
|
|
45
|
+
// Initialize Bottleneck limiter for rate limiting
|
|
46
|
+
const limiter = new bottleneck_1.default({
|
|
47
|
+
maxConcurrent: 1, // One request at a time per peer
|
|
48
|
+
minTime: 60000 / MAX_REQUESTS_PER_MINUTE, // e.g., 600 ms between requests for 100 requests/min
|
|
49
|
+
});
|
|
50
|
+
return new Proxy(peer, {
|
|
51
|
+
get(target, prop, receiver) {
|
|
52
|
+
const originalMethod = target[prop];
|
|
53
|
+
if (typeof originalMethod === "function") {
|
|
54
|
+
return async (...args) => {
|
|
55
|
+
try {
|
|
56
|
+
// Schedule the method call with Bottleneck's limiter
|
|
57
|
+
const result = await limiter.schedule(() => originalMethod.apply(target, args));
|
|
58
|
+
// On successful operation, increase the weight slightly
|
|
59
|
+
const fullNodePeer = FullNodePeer.getInstance();
|
|
60
|
+
const currentWeight = fullNodePeer.peerWeights.get(peerIP) || 1;
|
|
61
|
+
fullNodePeer.peerWeights.set(peerIP, currentWeight + 0.1); // Increment weight
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error(`Peer ${peerIP} encountered an error: ${error.message}`);
|
|
66
|
+
// Check if the error is related to WebSocket or Operation timed out
|
|
67
|
+
if (error.message.includes("WebSocket") ||
|
|
68
|
+
error.message.includes("Operation timed out")) {
|
|
69
|
+
// Handle the disconnection and mark the peer accordingly
|
|
70
|
+
FullNodePeer.getInstance().handlePeerDisconnection(peerIP);
|
|
71
|
+
// If maximum retries reached, throw the error
|
|
72
|
+
if (retryCount >= MAX_RETRIES) {
|
|
73
|
+
console.error(`Max retries reached for method ${String(prop)} on peer ${peerIP}.`);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
// Attempt to select a new peer and retry the method
|
|
77
|
+
try {
|
|
78
|
+
console.info(`Selecting a new peer to retry method ${String(prop)}...`);
|
|
79
|
+
const newPeer = await FullNodePeer.getInstance().getBestPeer();
|
|
80
|
+
// Extract new peer's IP address
|
|
81
|
+
const newPeerIP = FullNodePeer.getInstance().extractPeerIP(newPeer);
|
|
82
|
+
if (!newPeerIP) {
|
|
83
|
+
throw new Error("Unable to extract IP from the new peer.");
|
|
84
|
+
}
|
|
85
|
+
// Wrap the new peer with a proxy, incrementing the retry count
|
|
86
|
+
const proxiedNewPeer = createPeerProxy(newPeer, newPeerIP, retryCount + 1);
|
|
87
|
+
// Retry the method on the new peer
|
|
88
|
+
return await proxiedNewPeer[prop](...args);
|
|
89
|
+
}
|
|
90
|
+
catch (retryError) {
|
|
91
|
+
console.error(`Retry failed on a new peer: ${retryError.message}`);
|
|
92
|
+
throw retryError;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// For other errors, handle normally
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return originalMethod;
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* FullNodePeer manages connections to full nodes, prioritizing certain peers and handling reliability.
|
|
108
|
+
* It implements a singleton pattern to ensure a single instance throughout the application.
|
|
109
|
+
*/
|
|
27
110
|
class FullNodePeer {
|
|
28
|
-
|
|
29
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Private constructor for singleton pattern.
|
|
113
|
+
*/
|
|
114
|
+
constructor() {
|
|
115
|
+
// Cached peer with timestamp
|
|
116
|
+
this.cachedPeer = null;
|
|
117
|
+
// Cooldown cache to exclude faulty peers temporarily
|
|
118
|
+
this.cooldownCache = new node_cache_1.default({ stdTTL: COOLDOWN_DURATION / 1000 });
|
|
119
|
+
// Peer reliability weights
|
|
120
|
+
this.peerWeights = new Map();
|
|
121
|
+
// List of prioritized peers
|
|
122
|
+
this.prioritizedPeers = [];
|
|
123
|
+
// Map to store PeerInfo
|
|
124
|
+
this.peerInfos = new Map();
|
|
125
|
+
// Cache for fetched peer IPs
|
|
126
|
+
this.peerIPCache = new node_cache_1.default({ stdTTL: CACHE_DURATION / 1000 });
|
|
127
|
+
// List of available peers for round-robin
|
|
128
|
+
this.availablePeers = [];
|
|
129
|
+
// Current index for round-robin selection
|
|
130
|
+
this.currentPeerIndex = 0;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Retrieves the singleton instance of FullNodePeer.
|
|
134
|
+
* @returns {FullNodePeer} The singleton instance.
|
|
135
|
+
*/
|
|
136
|
+
static getInstance() {
|
|
137
|
+
if (!FullNodePeer.instance) {
|
|
138
|
+
FullNodePeer.instance = new FullNodePeer();
|
|
139
|
+
}
|
|
140
|
+
return FullNodePeer.instance;
|
|
30
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Initializes the singleton instance by connecting to the best peer.
|
|
144
|
+
*/
|
|
145
|
+
async initialize() {
|
|
146
|
+
if (this.cachedPeer)
|
|
147
|
+
return; // Already initialized
|
|
148
|
+
try {
|
|
149
|
+
const bestPeer = await this.getBestPeer();
|
|
150
|
+
this.cachedPeer = { peer: bestPeer, timestamp: Date.now() };
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
console.error(`Initialization failed: ${error.message}`);
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Connects and returns the best available peer.
|
|
159
|
+
* Implements singleton behavior.
|
|
160
|
+
* @returns {Promise<Peer>} The connected Peer instance.
|
|
161
|
+
*/
|
|
31
162
|
static async connect() {
|
|
32
|
-
const
|
|
33
|
-
|
|
163
|
+
const instance = FullNodePeer.getInstance();
|
|
164
|
+
await instance.initialize();
|
|
165
|
+
return instance.cachedPeer.peer;
|
|
34
166
|
}
|
|
35
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Checks if a given port on a host is reachable.
|
|
169
|
+
* @param {string} host - The host IP address.
|
|
170
|
+
* @param {number} port - The port number.
|
|
171
|
+
* @param {number} timeout - Connection timeout in milliseconds.
|
|
172
|
+
* @returns {Promise<boolean>} Whether the port is reachable.
|
|
173
|
+
*/
|
|
174
|
+
isPortReachable(host, port, timeout = CONNECTION_TIMEOUT) {
|
|
36
175
|
return new Promise((resolve) => {
|
|
37
176
|
const socket = new net_1.default.Socket()
|
|
38
177
|
.setTimeout(timeout)
|
|
39
|
-
.once("error", () =>
|
|
40
|
-
.
|
|
178
|
+
.once("error", () => {
|
|
179
|
+
socket.destroy();
|
|
180
|
+
resolve(false);
|
|
181
|
+
})
|
|
182
|
+
.once("timeout", () => {
|
|
183
|
+
socket.destroy();
|
|
184
|
+
resolve(false);
|
|
185
|
+
})
|
|
41
186
|
.connect(port, host, () => {
|
|
42
187
|
socket.end();
|
|
43
188
|
resolve(true);
|
|
44
189
|
});
|
|
45
190
|
});
|
|
46
191
|
}
|
|
47
|
-
|
|
48
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Validates an IPv4 address.
|
|
194
|
+
* @param {string} ip - The IP address to validate.
|
|
195
|
+
* @returns {boolean} Whether the IP address is valid.
|
|
196
|
+
*/
|
|
197
|
+
isValidIpAddress(ip) {
|
|
198
|
+
const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/;
|
|
49
199
|
return ipv4Regex.test(ip);
|
|
50
200
|
}
|
|
51
201
|
/**
|
|
52
|
-
* Retrieves the TRUSTED_FULLNODE IP from the environment
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* @returns {string | null} The valid IP address or null if invalid
|
|
202
|
+
* Retrieves the TRUSTED_FULLNODE IP from the environment and verifies its validity.
|
|
203
|
+
* @returns {string | null} The trusted full node IP or null if invalid.
|
|
56
204
|
*/
|
|
57
|
-
|
|
58
|
-
const trustedNodeIp =
|
|
59
|
-
if (trustedNodeIp &&
|
|
205
|
+
getTrustedFullNode() {
|
|
206
|
+
const trustedNodeIp = Environment_1.Environment.TRUSTED_FULLNODE || null;
|
|
207
|
+
if (trustedNodeIp && this.isValidIpAddress(trustedNodeIp)) {
|
|
60
208
|
return trustedNodeIp;
|
|
61
209
|
}
|
|
62
210
|
return null;
|
|
63
211
|
}
|
|
64
|
-
|
|
65
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Fetches new peer IPs from DNS introducers and prioritized hosts.
|
|
214
|
+
* Utilizes caching to avoid redundant DNS resolutions.
|
|
215
|
+
* @returns {Promise<string[]>} An array of reachable peer IPs.
|
|
216
|
+
*/
|
|
217
|
+
async fetchNewPeerIPs() {
|
|
218
|
+
const trustedNodeIp = this.getTrustedFullNode();
|
|
66
219
|
const priorityIps = [];
|
|
220
|
+
// Define prioritized peers
|
|
221
|
+
this.prioritizedPeers = [CHIA_NODES_HOST, LOCALHOST];
|
|
222
|
+
// Add trustedNodeIp if available
|
|
223
|
+
if (trustedNodeIp) {
|
|
224
|
+
this.prioritizedPeers.unshift(trustedNodeIp);
|
|
225
|
+
}
|
|
67
226
|
// Prioritize trustedNodeIp
|
|
68
227
|
if (trustedNodeIp &&
|
|
69
|
-
|
|
228
|
+
!this.cooldownCache.has(trustedNodeIp) &&
|
|
229
|
+
(await this.isPortReachable(trustedNodeIp, FULLNODE_PORT))) {
|
|
70
230
|
priorityIps.push(trustedNodeIp);
|
|
71
231
|
}
|
|
72
232
|
// Prioritize LOCALHOST
|
|
73
|
-
if (
|
|
233
|
+
if (!this.cooldownCache.has(LOCALHOST) &&
|
|
234
|
+
(await this.isPortReachable(LOCALHOST, FULLNODE_PORT))) {
|
|
74
235
|
priorityIps.push(LOCALHOST);
|
|
75
236
|
}
|
|
237
|
+
// Prioritize CHIA_NODES_HOST
|
|
238
|
+
if (!this.cooldownCache.has(CHIA_NODES_HOST) &&
|
|
239
|
+
(await this.isPortReachable(CHIA_NODES_HOST, FULLNODE_PORT))) {
|
|
240
|
+
priorityIps.push(CHIA_NODES_HOST);
|
|
241
|
+
}
|
|
76
242
|
if (priorityIps.length > 0) {
|
|
77
243
|
return priorityIps;
|
|
78
244
|
}
|
|
245
|
+
// Check if cached peer IPs exist
|
|
246
|
+
const cachedPeerIPs = this.peerIPCache.get("peerIPs");
|
|
247
|
+
if (cachedPeerIPs) {
|
|
248
|
+
return cachedPeerIPs;
|
|
249
|
+
}
|
|
79
250
|
// Fetch peers from DNS introducers
|
|
251
|
+
const fetchedPeers = [];
|
|
80
252
|
for (const DNS_HOST of DNS_HOSTS) {
|
|
81
253
|
try {
|
|
82
254
|
const ips = await (0, promises_1.resolve4)(DNS_HOST);
|
|
83
255
|
if (ips && ips.length > 0) {
|
|
84
|
-
const shuffledIps =
|
|
256
|
+
const shuffledIps = this.shuffleArray(ips);
|
|
85
257
|
const reachableIps = [];
|
|
86
258
|
for (const ip of shuffledIps) {
|
|
87
|
-
if (
|
|
259
|
+
if (!this.cooldownCache.has(ip) &&
|
|
260
|
+
(await this.isPortReachable(ip, FULLNODE_PORT))) {
|
|
88
261
|
reachableIps.push(ip);
|
|
89
262
|
}
|
|
90
|
-
if (reachableIps.length ===
|
|
91
|
-
break;
|
|
263
|
+
if (reachableIps.length === MAX_PEERS_TO_FETCH)
|
|
264
|
+
break; // Limit to MAX_PEERS_TO_FETCH peers
|
|
92
265
|
}
|
|
93
266
|
if (reachableIps.length > 0) {
|
|
94
|
-
|
|
267
|
+
fetchedPeers.push(...reachableIps);
|
|
95
268
|
}
|
|
96
269
|
}
|
|
97
270
|
}
|
|
@@ -99,131 +272,246 @@ class FullNodePeer {
|
|
|
99
272
|
console.error(`Failed to resolve IPs from ${DNS_HOST}: ${error.message}`);
|
|
100
273
|
}
|
|
101
274
|
}
|
|
275
|
+
// Cache the fetched peer IPs
|
|
276
|
+
if (fetchedPeers.length > 0) {
|
|
277
|
+
this.peerIPCache.set("peerIPs", fetchedPeers);
|
|
278
|
+
return fetchedPeers;
|
|
279
|
+
}
|
|
102
280
|
throw new Error("No reachable IPs found in any DNS records.");
|
|
103
281
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// Start the timeout to forget the peer after 1 minute
|
|
130
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
131
|
-
timeoutId = setTimeout(() => {
|
|
132
|
-
FullNodePeer.cachedPeer = null;
|
|
133
|
-
reject(new Error("Operation timed out. Reconnecting to a new peer."));
|
|
134
|
-
}, 60000); // 1 minute
|
|
135
|
-
});
|
|
136
|
-
try {
|
|
137
|
-
// Run the original method and race it against the timeout
|
|
138
|
-
const result = await Promise.race([
|
|
139
|
-
originalMethod.apply(target, args),
|
|
140
|
-
timeoutPromise,
|
|
141
|
-
]);
|
|
142
|
-
// Clear the timeout if the operation succeeded
|
|
143
|
-
if (timeoutId) {
|
|
144
|
-
clearTimeout(timeoutId);
|
|
145
|
-
}
|
|
146
|
-
return result;
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
// If the error is WebSocket-related or timeout, reset the peer
|
|
150
|
-
if (error.message.includes("WebSocket") || error.message.includes("Operation timed out")) {
|
|
151
|
-
FullNodePeer.cachedPeer = null;
|
|
152
|
-
// @ts-ignore
|
|
153
|
-
FullNodePeer.memoizedFetchNewPeerIPs.cache.clear();
|
|
154
|
-
const newPeer = await FullNodePeer.getBestPeer();
|
|
155
|
-
return newPeer[prop](...args);
|
|
156
|
-
}
|
|
157
|
-
throw error;
|
|
158
|
-
}
|
|
159
|
-
};
|
|
282
|
+
/**
|
|
283
|
+
* Shuffles an array using the Fisher-Yates algorithm.
|
|
284
|
+
* @param {T[]} array - The array to shuffle.
|
|
285
|
+
* @returns {T[]} The shuffled array.
|
|
286
|
+
*/
|
|
287
|
+
shuffleArray(array) {
|
|
288
|
+
const shuffled = [...array];
|
|
289
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
290
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
291
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
292
|
+
}
|
|
293
|
+
return shuffled;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Initializes the peer weights map.
|
|
297
|
+
* Assigns higher initial weights to prioritized peers.
|
|
298
|
+
* @param {string[]} peerIPs - An array of peer IPs.
|
|
299
|
+
*/
|
|
300
|
+
initializePeerWeights(peerIPs) {
|
|
301
|
+
for (const ip of peerIPs) {
|
|
302
|
+
if (!this.peerWeights.has(ip)) {
|
|
303
|
+
if (ip === CHIA_NODES_HOST ||
|
|
304
|
+
ip === LOCALHOST ||
|
|
305
|
+
ip === this.getTrustedFullNode()) {
|
|
306
|
+
this.peerWeights.set(ip, 5); // Higher weight for prioritized peers
|
|
160
307
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
308
|
+
else {
|
|
309
|
+
this.peerWeights.set(ip, 1); // Default weight
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
164
313
|
}
|
|
165
|
-
|
|
314
|
+
/**
|
|
315
|
+
* Selects the next peer based on round-robin selection.
|
|
316
|
+
* @returns {Promise<Peer>} The selected Peer instance.
|
|
317
|
+
*/
|
|
318
|
+
async selectNextPeer() {
|
|
319
|
+
if (this.availablePeers.length === 0) {
|
|
320
|
+
throw new Error("No available peers to select.");
|
|
321
|
+
}
|
|
322
|
+
const peerIP = this.availablePeers[this.currentPeerIndex];
|
|
323
|
+
this.currentPeerIndex = (this.currentPeerIndex + 1) % this.availablePeers.length;
|
|
324
|
+
const peerInfo = this.peerInfos.get(peerIP);
|
|
325
|
+
return peerInfo.peer;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Retrieves all reachable peer IPs, excluding those in cooldown.
|
|
329
|
+
* @returns {Promise<string[]>} An array of reachable peer IPs.
|
|
330
|
+
*/
|
|
331
|
+
async getPeerIPs() {
|
|
332
|
+
const peerIPs = await this.fetchNewPeerIPs();
|
|
333
|
+
return peerIPs;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Initializes the peer weights based on prioritization and reliability.
|
|
337
|
+
* @param {string[]} peerIPs - An array of peer IPs.
|
|
338
|
+
*/
|
|
339
|
+
setupPeers(peerIPs) {
|
|
340
|
+
this.initializePeerWeights(peerIPs);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Connects to the best available peer based on round-robin selection and reliability.
|
|
344
|
+
* @returns {Promise<Peer>} The connected Peer instance.
|
|
345
|
+
*/
|
|
346
|
+
async getBestPeer() {
|
|
166
347
|
const now = Date.now();
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
348
|
+
// Refresh cachedPeer if expired or disconnected
|
|
349
|
+
if (this.cachedPeer &&
|
|
350
|
+
now - this.cachedPeer.timestamp < CACHE_DURATION &&
|
|
351
|
+
this.peerInfos.get(this.extractPeerIP(this.cachedPeer.peer) || "")?.isConnected) {
|
|
352
|
+
return this.cachedPeer.peer;
|
|
353
|
+
}
|
|
354
|
+
// Fetch peer IPs
|
|
355
|
+
const peerIPs = await this.getPeerIPs();
|
|
356
|
+
// Setup peer weights with prioritization
|
|
357
|
+
this.setupPeers(peerIPs);
|
|
358
|
+
// Initialize or update peerInfos and availablePeers
|
|
359
|
+
for (const ip of peerIPs) {
|
|
360
|
+
if (!this.peerInfos.has(ip)) {
|
|
361
|
+
// Attempt to create a peer connection
|
|
362
|
+
const sslFolder = path_1.default.resolve(os_1.default.homedir(), ".dig", "ssl");
|
|
363
|
+
const certFile = path_1.default.join(sslFolder, "public_dig.crt");
|
|
364
|
+
const keyFile = path_1.default.join(sslFolder, "public_dig.key");
|
|
365
|
+
if (!fs_1.default.existsSync(sslFolder)) {
|
|
366
|
+
fs_1.default.mkdirSync(sslFolder, { recursive: true });
|
|
367
|
+
}
|
|
368
|
+
const tls = new datalayer_driver_1.Tls(certFile, keyFile);
|
|
369
|
+
let peer;
|
|
182
370
|
try {
|
|
183
|
-
|
|
184
|
-
return FullNodePeer.createPeerProxy(peer);
|
|
371
|
+
peer = await datalayer_driver_1.Peer.new(`${ip}:${FULLNODE_PORT}`, false, tls);
|
|
185
372
|
}
|
|
186
373
|
catch (error) {
|
|
187
374
|
console.error(`Failed to create peer for IP ${ip}: ${error.message}`);
|
|
188
|
-
|
|
375
|
+
// Add to cooldown
|
|
376
|
+
this.cooldownCache.set(ip, true);
|
|
377
|
+
// Decrease weight or remove peer
|
|
378
|
+
const currentWeight = this.peerWeights.get(ip) || 1;
|
|
379
|
+
if (currentWeight > 1) {
|
|
380
|
+
this.peerWeights.set(ip, currentWeight - 1);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
this.peerWeights.delete(ip);
|
|
384
|
+
}
|
|
385
|
+
continue; // Skip adding this peer
|
|
189
386
|
}
|
|
387
|
+
// Wrap the peer with proxy to handle errors and retries
|
|
388
|
+
const proxiedPeer = createPeerProxy(peer, ip);
|
|
389
|
+
// Store PeerInfo
|
|
390
|
+
this.peerInfos.set(ip, {
|
|
391
|
+
peer: proxiedPeer,
|
|
392
|
+
weight: this.peerWeights.get(ip) || 1,
|
|
393
|
+
address: ip,
|
|
394
|
+
isConnected: true, // Mark as connected
|
|
395
|
+
limiter: proxiedPeer.limiter, // Assign the limiter from Proxy
|
|
396
|
+
});
|
|
397
|
+
// Add to availablePeers
|
|
398
|
+
this.availablePeers.push(ip);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
const peerInfo = this.peerInfos.get(ip);
|
|
402
|
+
if (!peerInfo.isConnected) {
|
|
403
|
+
// Peer is back from cooldown, re-establish connection
|
|
404
|
+
const sslFolder = path_1.default.resolve(os_1.default.homedir(), ".dig", "ssl");
|
|
405
|
+
const certFile = path_1.default.join(sslFolder, "public_dig.crt");
|
|
406
|
+
const keyFile = path_1.default.join(sslFolder, "public_dig.key");
|
|
407
|
+
if (!fs_1.default.existsSync(sslFolder)) {
|
|
408
|
+
fs_1.default.mkdirSync(sslFolder, { recursive: true });
|
|
409
|
+
}
|
|
410
|
+
const tls = new datalayer_driver_1.Tls(certFile, keyFile);
|
|
411
|
+
let peer;
|
|
412
|
+
try {
|
|
413
|
+
peer = await datalayer_driver_1.Peer.new(`${ip}:${FULLNODE_PORT}`, false, tls);
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
console.error(`Failed to reconnect peer for IP ${ip}: ${error.message}`);
|
|
417
|
+
// Re-add to cooldown
|
|
418
|
+
this.cooldownCache.set(ip, true);
|
|
419
|
+
// Decrease weight or remove peer
|
|
420
|
+
const currentWeight = this.peerWeights.get(ip) || 1;
|
|
421
|
+
if (currentWeight > 1) {
|
|
422
|
+
this.peerWeights.set(ip, currentWeight - 1);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
this.peerWeights.delete(ip);
|
|
426
|
+
}
|
|
427
|
+
continue; // Skip adding this peer
|
|
428
|
+
}
|
|
429
|
+
// Wrap the peer with proxy to handle errors and retries
|
|
430
|
+
const proxiedPeer = createPeerProxy(peer, ip);
|
|
431
|
+
// Update PeerInfo
|
|
432
|
+
peerInfo.peer = proxiedPeer;
|
|
433
|
+
peerInfo.isConnected = true;
|
|
434
|
+
// Add back to availablePeers
|
|
435
|
+
this.availablePeers.push(ip);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (this.availablePeers.length === 0) {
|
|
440
|
+
throw new Error("No available peers to connect.");
|
|
441
|
+
}
|
|
442
|
+
// Select the next peer in round-robin
|
|
443
|
+
const selectedPeer = await this.selectNextPeer();
|
|
444
|
+
// Cache the peer
|
|
445
|
+
this.cachedPeer = { peer: selectedPeer, timestamp: now };
|
|
446
|
+
console.log(`Using Fullnode Peer: ${this.extractPeerIP(selectedPeer)}`);
|
|
447
|
+
return selectedPeer;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Handles peer disconnection by marking it in cooldown and updating internal states.
|
|
451
|
+
* @param {string} peerIP - The IP address of the disconnected peer.
|
|
452
|
+
*/
|
|
453
|
+
handlePeerDisconnection(peerIP) {
|
|
454
|
+
// Mark the peer in cooldown
|
|
455
|
+
this.cooldownCache.set(peerIP, true);
|
|
456
|
+
// Decrease weight or remove peer
|
|
457
|
+
const currentWeight = this.peerWeights.get(peerIP) || 1;
|
|
458
|
+
if (currentWeight > 1) {
|
|
459
|
+
this.peerWeights.set(peerIP, currentWeight - 1);
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
this.peerWeights.delete(peerIP);
|
|
463
|
+
}
|
|
464
|
+
// Update the peer's connection status
|
|
465
|
+
const peerInfo = this.peerInfos.get(peerIP);
|
|
466
|
+
if (peerInfo) {
|
|
467
|
+
peerInfo.isConnected = false;
|
|
468
|
+
this.peerInfos.set(peerIP, peerInfo);
|
|
469
|
+
}
|
|
470
|
+
// Remove from availablePeers if present
|
|
471
|
+
const index = this.availablePeers.indexOf(peerIP);
|
|
472
|
+
if (index !== -1) {
|
|
473
|
+
this.availablePeers.splice(index, 1);
|
|
474
|
+
// Adjust currentPeerIndex if necessary
|
|
475
|
+
if (this.currentPeerIndex >= this.availablePeers.length) {
|
|
476
|
+
this.currentPeerIndex = 0;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// If the disconnected peer was the cached peer, invalidate the cache
|
|
480
|
+
if (this.cachedPeer &&
|
|
481
|
+
this.extractPeerIP(this.cachedPeer.peer) === peerIP) {
|
|
482
|
+
this.cachedPeer = null;
|
|
483
|
+
}
|
|
484
|
+
console.warn(`Peer ${peerIP} has been marked as disconnected and is in cooldown.`);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Extracts the IP address from a Peer instance.
|
|
488
|
+
* @param {Peer} peer - The Peer instance.
|
|
489
|
+
* @returns {string | null} The extracted IP address or null if not found.
|
|
490
|
+
*/
|
|
491
|
+
extractPeerIP(peer) {
|
|
492
|
+
for (const [ip, info] of this.peerInfos.entries()) {
|
|
493
|
+
if (info.peer === peer) {
|
|
494
|
+
return ip;
|
|
190
495
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (peers.length === 0) {
|
|
194
|
-
throw new Error("No peers available, please try again.");
|
|
195
|
-
}
|
|
196
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
197
|
-
const peakHeights = await Promise.all(peers.map((peer) => peer
|
|
198
|
-
.getPeak()
|
|
199
|
-
.then((height) => height)
|
|
200
|
-
.catch((error) => {
|
|
201
|
-
console.error(`Failed to get peak for peer: ${error.message}`);
|
|
202
|
-
return null;
|
|
203
|
-
})));
|
|
204
|
-
const validHeights = peakHeights.filter((height) => height !== null);
|
|
205
|
-
if (validHeights.length === 0) {
|
|
206
|
-
throw new Error("No valid peak heights obtained from any peer.");
|
|
207
|
-
}
|
|
208
|
-
const highestPeak = Math.max(...validHeights);
|
|
209
|
-
// Prioritize LOCALHOST and TRUSTED_NODE_IP if they have the highest peak height
|
|
210
|
-
let bestPeerIndex = validHeights.findIndex((height, index) => height === highestPeak &&
|
|
211
|
-
(peerIPs[index] === LOCALHOST || peerIPs[index] === trustedNodeIp));
|
|
212
|
-
// If LOCALHOST or TRUSTED_NODE_IP don't have the highest peak, select any peer with the highest peak
|
|
213
|
-
if (bestPeerIndex === -1) {
|
|
214
|
-
bestPeerIndex = validHeights.findIndex((height) => height === highestPeak);
|
|
215
|
-
}
|
|
216
|
-
const bestPeer = peers[bestPeerIndex];
|
|
217
|
-
FullNodePeer.cachedPeer = { peer: bestPeer, timestamp: now };
|
|
218
|
-
console.log(`Using Peer: ${peerIPs[bestPeerIndex]}`);
|
|
219
|
-
return bestPeer;
|
|
220
|
-
}
|
|
221
|
-
getPeer() {
|
|
222
|
-
return this.peer;
|
|
496
|
+
}
|
|
497
|
+
return null;
|
|
223
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Waits for a coin to be confirmed (spent) on the blockchain.
|
|
501
|
+
* @param {Buffer} parentCoinInfo - The parent coin information.
|
|
502
|
+
* @returns {Promise<boolean>} Whether the coin was confirmed.
|
|
503
|
+
*/
|
|
224
504
|
static async waitForConfirmation(parentCoinInfo) {
|
|
225
505
|
const spinner = (0, nanospinner_1.createSpinner)("Waiting for confirmation...").start();
|
|
226
|
-
|
|
506
|
+
let peer;
|
|
507
|
+
try {
|
|
508
|
+
peer = await FullNodePeer.connect();
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
spinner.error({ text: "Failed to connect to a peer." });
|
|
512
|
+
console.error(`waitForConfirmation connection error: ${error.message}`);
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
227
515
|
try {
|
|
228
516
|
while (true) {
|
|
229
517
|
const confirmed = await peer.isCoinSpent(parentCoinInfo, config_1.MIN_HEIGHT, Buffer.from(config_1.MIN_HEIGHT_HEADER_HASH, "hex"));
|
|
@@ -231,17 +519,24 @@ class FullNodePeer {
|
|
|
231
519
|
spinner.success({ text: "Coin confirmed!" });
|
|
232
520
|
return true;
|
|
233
521
|
}
|
|
234
|
-
await
|
|
522
|
+
await FullNodePeer.delay(5000);
|
|
235
523
|
}
|
|
236
524
|
}
|
|
237
525
|
catch (error) {
|
|
238
526
|
spinner.error({ text: "Error while waiting for confirmation." });
|
|
527
|
+
console.error(`waitForConfirmation error: ${error.message}`);
|
|
239
528
|
throw error;
|
|
240
529
|
}
|
|
241
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Delays execution for a specified amount of time.
|
|
533
|
+
* @param {number} ms - Milliseconds to delay.
|
|
534
|
+
* @returns {Promise<void>} A promise that resolves after the delay.
|
|
535
|
+
*/
|
|
536
|
+
static delay(ms) {
|
|
537
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
538
|
+
}
|
|
242
539
|
}
|
|
243
540
|
exports.FullNodePeer = FullNodePeer;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
FullNodePeer.memoizedFetchNewPeerIPs = (0, lodash_1.memoize)(FullNodePeer.fetchNewPeerIPs);
|
|
247
|
-
})();
|
|
541
|
+
// Singleton instance
|
|
542
|
+
FullNodePeer.instance = null;
|