@dignetwork/dig-sdk 0.0.1-alpha.134 → 0.0.1-alpha.136
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/DigNetwork/PropagationServer.d.ts.map +1 -1
- package/dist/DigNetwork/PropagationServer.js +2 -27
- package/dist/blockchain/DataStore.js +2 -2
- package/dist/blockchain/FullNodePeer.d.ts +21 -9
- package/dist/blockchain/FullNodePeer.d.ts.map +1 -1
- package/dist/blockchain/FullNodePeer.js +233 -114
- package/package.json +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PropagationServer.d.ts","sourceRoot":"","sources":["../../src/DigNetwork/PropagationServer.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAC;AAY1B,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAkBrC,qBAAa,iBAAiB;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,GAAG,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAE7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAQ;IACpC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAQ;gBAErC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAY9C;;OAEG;IACG,gBAAgB;IAOtB;;OAEG;IACH,gBAAgB;IAQhB;;;OAGG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BpD;;;OAGG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"PropagationServer.d.ts","sourceRoot":"","sources":["../../src/DigNetwork/PropagationServer.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAC;AAY1B,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAkBrC,qBAAa,iBAAiB;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,GAAG,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAE7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAQ;IACpC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAQ;gBAErC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAY9C;;OAEG;IACG,gBAAgB;IAOtB;;OAEG;IACH,gBAAgB;IAQhB;;;OAGG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BpD;;;OAGG;IACG,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmCjD;;OAEG;IACH,oBAAoB,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM;IAyB3D;;OAEG;IACG,gBAAgB,CACpB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,WAAW,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,OAAO,CAAA;KAAE,CAAC;IA4C7D;;OAEG;IACG,kBAAkB,CAAC,QAAQ,EAAE,MAAM;IAyDzC;;OAEG;IACG,YAAY,CAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAC;IA2BlD;;;OAGG;IACG,UAAU,CACd,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,UAAU,GAAE,OAAe;IAsH7B;;OAEG;IACG,mBAAmB;IAkCzB;;OAEG;WACU,WAAW,CACtB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM;IA0DnB;;;OAGG;IACG,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAkFlD;;OAEG;IACG,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IA4B7C;;;OAGG;IACG,YAAY,CAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM;IA0GjB;;OAEG;WACU,aAAa,CACxB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM;CA2FpB"}
|
|
@@ -128,10 +128,6 @@ class PropagationServer {
|
|
|
128
128
|
headers: {
|
|
129
129
|
"Content-Type": "application/json",
|
|
130
130
|
},
|
|
131
|
-
validateStatus: (status) => {
|
|
132
|
-
// Accept all status codes to handle them manually
|
|
133
|
-
return true;
|
|
134
|
-
},
|
|
135
131
|
};
|
|
136
132
|
// Data to send in the request (storeId and rootHash)
|
|
137
133
|
const data = {
|
|
@@ -141,31 +137,10 @@ class PropagationServer {
|
|
|
141
137
|
};
|
|
142
138
|
try {
|
|
143
139
|
const response = await axios_1.default.post(url, data, config);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
else if (response.status === 400 &&
|
|
149
|
-
response.data?.error?.includes("does not exist")) {
|
|
150
|
-
console.log((0, colorette_1.yellow)(`⚠ Peer ${this.ipAddress} does not have store ${this.storeId}. Notifying for update.`));
|
|
151
|
-
// You can implement additional logic here if needed, such as retrying or logging
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
console.error((0, colorette_1.red)(`✖ Unexpected response from peer ${this.ipAddress}: ${response.status} ${response.statusText}`));
|
|
156
|
-
throw new Error(`Unexpected response: ${response.status} ${response.statusText}`);
|
|
157
|
-
}
|
|
140
|
+
console.log((0, colorette_1.green)(`✔ Successfully pinged peer: ${this.ipAddress}`));
|
|
141
|
+
return response.data;
|
|
158
142
|
}
|
|
159
143
|
catch (error) {
|
|
160
|
-
if (error.response) {
|
|
161
|
-
// Server responded with a status other than 2xx
|
|
162
|
-
if (error.response.status === 400 &&
|
|
163
|
-
error.response.data?.error?.includes("does not exist")) {
|
|
164
|
-
console.log((0, colorette_1.yellow)(`⚠ Peer ${this.ipAddress} does not have store ${this.storeId}. Notifying for update.`));
|
|
165
|
-
// You can implement additional logic here if needed
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
144
|
console.error((0, colorette_1.red)(`✖ Failed to ping peer: ${this.ipAddress}`));
|
|
170
145
|
console.error((0, colorette_1.red)(error.message));
|
|
171
146
|
throw error;
|
|
@@ -197,9 +197,9 @@ class DataStore {
|
|
|
197
197
|
}
|
|
198
198
|
static getAllStores() {
|
|
199
199
|
const storeFolders = fs_1.default.readdirSync(config_1.STORE_PATH);
|
|
200
|
-
const
|
|
200
|
+
const storeIds = storeFolders.filter((folder) => /^[a-f0-9]{64}$/.test(folder) &&
|
|
201
201
|
fs_1.default.lstatSync(path_1.default.join(config_1.STORE_PATH, folder)).isDirectory());
|
|
202
|
-
return
|
|
202
|
+
return storeIds.map((storeId) => DataStore.from(storeId));
|
|
203
203
|
}
|
|
204
204
|
/**
|
|
205
205
|
* Fetches the latest coin information using the StoreMonitorRegistry.
|
|
@@ -11,6 +11,8 @@ export declare class FullNodePeer {
|
|
|
11
11
|
private static prioritizedPeers;
|
|
12
12
|
private static peerInfos;
|
|
13
13
|
private static peerIPCache;
|
|
14
|
+
private static availablePeers;
|
|
15
|
+
private static currentPeerIndex;
|
|
14
16
|
private constructor();
|
|
15
17
|
/**
|
|
16
18
|
* Retrieves the singleton instance of FullNodePeer.
|
|
@@ -65,11 +67,10 @@ export declare class FullNodePeer {
|
|
|
65
67
|
*/
|
|
66
68
|
private static initializePeerWeights;
|
|
67
69
|
/**
|
|
68
|
-
* Selects
|
|
69
|
-
* Prioritized peers have higher weights.
|
|
70
|
+
* Selects the next peer based on round-robin selection.
|
|
70
71
|
* @returns {string} The selected peer IP.
|
|
71
72
|
*/
|
|
72
|
-
private static
|
|
73
|
+
private static getNextPeerIP;
|
|
73
74
|
/**
|
|
74
75
|
* Retrieves all reachable peer IPs, excluding those in cooldown.
|
|
75
76
|
* @returns {Promise<string[]>} An array of reachable peer IPs.
|
|
@@ -81,18 +82,23 @@ export declare class FullNodePeer {
|
|
|
81
82
|
*/
|
|
82
83
|
private static setupPeers;
|
|
83
84
|
/**
|
|
84
|
-
* Connects to the best available peer based on
|
|
85
|
+
* Connects to the best available peer based on round-robin selection and reliability.
|
|
85
86
|
* @returns {Promise<Peer>} The connected Peer instance.
|
|
86
87
|
*/
|
|
87
88
|
private static getBestPeer;
|
|
88
89
|
/**
|
|
89
|
-
* Creates a proxy for the peer to handle errors and
|
|
90
|
+
* Creates a proxy for the peer to handle errors, implement retries, and enforce per-peer throttling.
|
|
90
91
|
* @param {Peer} peer - The Peer instance.
|
|
91
92
|
* @param {string} peerIP - The IP address of the peer.
|
|
92
93
|
* @param {number} [retryCount=0] - The current retry attempt.
|
|
93
94
|
* @returns {Peer} The proxied Peer instance.
|
|
94
95
|
*/
|
|
95
96
|
private static createPeerProxy;
|
|
97
|
+
/**
|
|
98
|
+
* Handles peer disconnection by marking it in cooldown and updating internal states.
|
|
99
|
+
* @param {string} peerIP - The IP address of the disconnected peer.
|
|
100
|
+
*/
|
|
101
|
+
private static handlePeerDisconnection;
|
|
96
102
|
/**
|
|
97
103
|
* Extracts the IP address from a Peer instance.
|
|
98
104
|
* @param {Peer} peer - The Peer instance.
|
|
@@ -100,10 +106,16 @@ export declare class FullNodePeer {
|
|
|
100
106
|
*/
|
|
101
107
|
private static extractPeerIP;
|
|
102
108
|
/**
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
* Waits for a coin to be confirmed (spent) on the blockchain.
|
|
110
|
+
* @param {Buffer} parentCoinInfo - The parent coin information.
|
|
111
|
+
* @returns {Promise<boolean>} Whether the coin was confirmed.
|
|
112
|
+
*/
|
|
107
113
|
static waitForConfirmation(parentCoinInfo: Buffer): Promise<boolean>;
|
|
114
|
+
/**
|
|
115
|
+
* Delays execution for a specified amount of time.
|
|
116
|
+
* @param {number} ms - Milliseconds to delay.
|
|
117
|
+
* @returns {Promise<void>} A promise that resolves after the delay.
|
|
118
|
+
*/
|
|
119
|
+
private static delay;
|
|
108
120
|
}
|
|
109
121
|
//# sourceMappingURL=FullNodePeer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FullNodePeer.d.ts","sourceRoot":"","sources":["../../src/blockchain/FullNodePeer.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAO,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"FullNodePeer.d.ts","sourceRoot":"","sources":["../../src/blockchain/FullNodePeer.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAO,MAAM,8BAA8B,CAAC;AAqCzD;;GAEG;AACH,qBAAa,YAAY;IA6BH,OAAO,CAAC,IAAI;IA3BhC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA6B;IAGpD,OAAO,CAAC,MAAM,CAAC,UAAU,CAAkD;IAG3E,OAAO,CAAC,MAAM,CAAC,aAAa,CAAuD;IAGnF,OAAO,CAAC,MAAM,CAAC,WAAW,CAAkC;IAG5D,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAgB;IAG/C,OAAO,CAAC,MAAM,CAAC,SAAS,CAAoC;IAG5D,OAAO,CAAC,MAAM,CAAC,WAAW,CAAoD;IAG9E,OAAO,CAAC,MAAM,CAAC,cAAc,CAAgB;IAG7C,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAa;IAG5C,OAAO;IAEP;;;OAGG;WACW,WAAW,IAAI,YAAY;IAOzC;;OAEG;IACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAaxC;;;;OAIG;WACiB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5C;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe;IAuB9B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAM/B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IASjC;;;;OAIG;mBACkB,eAAe;IAyFpC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,YAAY;IAS3B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAgBpC;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IAS5B;;;OAGG;mBACkB,UAAU;IAK/B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,UAAU;IAIzB;;;OAGG;mBACkB,WAAW;IAiIhC;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe;IAgG9B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,uBAAuB;IAwCtC;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IAS5B;;;;OAIG;WACiB,mBAAmB,CACrC,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,OAAO,CAAC;IAkCnB;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,KAAK;CAGrB"}
|
|
@@ -14,6 +14,7 @@ const nanospinner_1 = require("nanospinner");
|
|
|
14
14
|
const config_1 = require("../utils/config");
|
|
15
15
|
const Environment_1 = require("../utils/Environment");
|
|
16
16
|
const node_cache_1 = __importDefault(require("node-cache"));
|
|
17
|
+
const bottleneck_1 = __importDefault(require("bottleneck"));
|
|
17
18
|
// Constants
|
|
18
19
|
const FULLNODE_PORT = 8444;
|
|
19
20
|
const LOCALHOST = "127.0.0.1";
|
|
@@ -26,9 +27,10 @@ const DNS_HOSTS = [
|
|
|
26
27
|
];
|
|
27
28
|
const CONNECTION_TIMEOUT = 2000; // in milliseconds
|
|
28
29
|
const CACHE_DURATION = 30000; // in milliseconds
|
|
29
|
-
const COOLDOWN_DURATION =
|
|
30
|
+
const COOLDOWN_DURATION = 300000; // 5 minutes in milliseconds
|
|
30
31
|
const MAX_PEERS_TO_FETCH = 5; // Maximum number of peers to fetch from DNS
|
|
31
32
|
const MAX_RETRIES = 3; // Maximum number of retry attempts
|
|
33
|
+
const MAX_REQUESTS_PER_MINUTE = 100; // Per-peer rate limit
|
|
32
34
|
/**
|
|
33
35
|
* FullNodePeer manages connections to full nodes, prioritizing certain peers and handling reliability.
|
|
34
36
|
*/
|
|
@@ -224,28 +226,16 @@ class FullNodePeer {
|
|
|
224
226
|
}
|
|
225
227
|
}
|
|
226
228
|
/**
|
|
227
|
-
* Selects
|
|
228
|
-
* Prioritized peers have higher weights.
|
|
229
|
+
* Selects the next peer based on round-robin selection.
|
|
229
230
|
* @returns {string} The selected peer IP.
|
|
230
231
|
*/
|
|
231
|
-
static
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
.map(([ip, weight]) => ({ ip, weight }));
|
|
235
|
-
const totalWeight = peers.reduce((sum, peer) => sum + peer.weight, 0);
|
|
236
|
-
if (totalWeight === 0) {
|
|
237
|
-
throw new Error("All peers are in cooldown.");
|
|
232
|
+
static getNextPeerIP() {
|
|
233
|
+
if (FullNodePeer.availablePeers.length === 0) {
|
|
234
|
+
throw new Error("No available peers to select.");
|
|
238
235
|
}
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
cumulative += peer.weight;
|
|
243
|
-
if (random < cumulative) {
|
|
244
|
-
return peer.ip;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
// Fallback
|
|
248
|
-
return peers[peers.length - 1].ip;
|
|
236
|
+
const peerIP = FullNodePeer.availablePeers[FullNodePeer.currentPeerIndex];
|
|
237
|
+
FullNodePeer.currentPeerIndex = (FullNodePeer.currentPeerIndex + 1) % FullNodePeer.availablePeers.length;
|
|
238
|
+
return peerIP;
|
|
249
239
|
}
|
|
250
240
|
/**
|
|
251
241
|
* Retrieves all reachable peer IPs, excluding those in cooldown.
|
|
@@ -263,137 +253,246 @@ class FullNodePeer {
|
|
|
263
253
|
FullNodePeer.initializePeerWeights(peerIPs);
|
|
264
254
|
}
|
|
265
255
|
/**
|
|
266
|
-
* Connects to the best available peer based on
|
|
256
|
+
* Connects to the best available peer based on round-robin selection and reliability.
|
|
267
257
|
* @returns {Promise<Peer>} The connected Peer instance.
|
|
268
258
|
*/
|
|
269
259
|
static async getBestPeer() {
|
|
270
260
|
const now = Date.now();
|
|
271
|
-
// Refresh cachedPeer if expired
|
|
261
|
+
// Refresh cachedPeer if expired or disconnected
|
|
272
262
|
if (FullNodePeer.cachedPeer &&
|
|
273
|
-
now - FullNodePeer.cachedPeer.timestamp < CACHE_DURATION
|
|
263
|
+
now - FullNodePeer.cachedPeer.timestamp < CACHE_DURATION &&
|
|
264
|
+
FullNodePeer.peerInfos.get(FullNodePeer.extractPeerIP(FullNodePeer.cachedPeer.peer) || "")?.isConnected) {
|
|
274
265
|
return FullNodePeer.cachedPeer.peer;
|
|
275
266
|
}
|
|
276
267
|
// Fetch peer IPs
|
|
277
268
|
const peerIPs = await FullNodePeer.getPeerIPs();
|
|
278
269
|
// Setup peer weights with prioritization
|
|
279
270
|
FullNodePeer.setupPeers(peerIPs);
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
271
|
+
// Initialize or update peerInfos and availablePeers
|
|
272
|
+
for (const ip of peerIPs) {
|
|
273
|
+
if (!FullNodePeer.peerInfos.has(ip)) {
|
|
274
|
+
// Create a new Bottleneck limiter for the peer
|
|
275
|
+
const limiter = new bottleneck_1.default({
|
|
276
|
+
maxConcurrent: 1, // One request at a time per peer
|
|
277
|
+
minTime: 60000 / MAX_REQUESTS_PER_MINUTE, // 600 ms between requests for 100 requests/min
|
|
278
|
+
});
|
|
279
|
+
// Attempt to create a peer connection
|
|
280
|
+
const sslFolder = path_1.default.resolve(os_1.default.homedir(), ".dig", "ssl");
|
|
281
|
+
const certFile = path_1.default.join(sslFolder, "public_dig.crt");
|
|
282
|
+
const keyFile = path_1.default.join(sslFolder, "public_dig.key");
|
|
283
|
+
if (!fs_1.default.existsSync(sslFolder)) {
|
|
284
|
+
fs_1.default.mkdirSync(sslFolder, { recursive: true });
|
|
285
|
+
}
|
|
286
|
+
const tls = new datalayer_driver_1.Tls(certFile, keyFile);
|
|
287
|
+
let peer;
|
|
288
|
+
try {
|
|
289
|
+
peer = await datalayer_driver_1.Peer.new(`${ip}:${FULLNODE_PORT}`, false, tls);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
console.error(`Failed to create peer for IP ${ip}: ${error.message}`);
|
|
293
|
+
// Add to cooldown
|
|
294
|
+
FullNodePeer.cooldownCache.set(ip, true);
|
|
295
|
+
// Decrease weight or remove peer
|
|
296
|
+
const currentWeight = FullNodePeer.peerWeights.get(ip) || 1;
|
|
297
|
+
if (currentWeight > 1) {
|
|
298
|
+
FullNodePeer.peerWeights.set(ip, currentWeight - 1);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
FullNodePeer.peerWeights.delete(ip);
|
|
302
|
+
}
|
|
303
|
+
continue; // Skip adding this peer
|
|
304
|
+
}
|
|
305
|
+
// Wrap the peer with proxy to handle errors and retries
|
|
306
|
+
const proxiedPeer = FullNodePeer.createPeerProxy(peer, ip);
|
|
307
|
+
// Store PeerInfo
|
|
308
|
+
FullNodePeer.peerInfos.set(ip, {
|
|
309
|
+
peer: proxiedPeer,
|
|
310
|
+
weight: FullNodePeer.peerWeights.get(ip) || 1,
|
|
311
|
+
address: ip,
|
|
312
|
+
isConnected: true, // Mark as connected
|
|
313
|
+
limiter, // Assign the limiter
|
|
314
|
+
});
|
|
315
|
+
// Add to availablePeers
|
|
316
|
+
FullNodePeer.availablePeers.push(ip);
|
|
308
317
|
}
|
|
309
318
|
else {
|
|
310
|
-
FullNodePeer.
|
|
319
|
+
const peerInfo = FullNodePeer.peerInfos.get(ip);
|
|
320
|
+
if (!peerInfo.isConnected) {
|
|
321
|
+
// Peer is back from cooldown, re-establish connection
|
|
322
|
+
const sslFolder = path_1.default.resolve(os_1.default.homedir(), ".dig", "ssl");
|
|
323
|
+
const certFile = path_1.default.join(sslFolder, "public_dig.crt");
|
|
324
|
+
const keyFile = path_1.default.join(sslFolder, "public_dig.key");
|
|
325
|
+
if (!fs_1.default.existsSync(sslFolder)) {
|
|
326
|
+
fs_1.default.mkdirSync(sslFolder, { recursive: true });
|
|
327
|
+
}
|
|
328
|
+
const tls = new datalayer_driver_1.Tls(certFile, keyFile);
|
|
329
|
+
let peer;
|
|
330
|
+
try {
|
|
331
|
+
peer = await datalayer_driver_1.Peer.new(`${ip}:${FULLNODE_PORT}`, false, tls);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
console.error(`Failed to reconnect peer for IP ${ip}: ${error.message}`);
|
|
335
|
+
// Re-add to cooldown
|
|
336
|
+
FullNodePeer.cooldownCache.set(ip, true);
|
|
337
|
+
// Decrease weight or remove peer
|
|
338
|
+
const currentWeight = FullNodePeer.peerWeights.get(ip) || 1;
|
|
339
|
+
if (currentWeight > 1) {
|
|
340
|
+
FullNodePeer.peerWeights.set(ip, currentWeight - 1);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
FullNodePeer.peerWeights.delete(ip);
|
|
344
|
+
}
|
|
345
|
+
continue; // Skip adding this peer
|
|
346
|
+
}
|
|
347
|
+
// Wrap the peer with proxy to handle errors and retries
|
|
348
|
+
const proxiedPeer = FullNodePeer.createPeerProxy(peer, ip);
|
|
349
|
+
// Update PeerInfo
|
|
350
|
+
peerInfo.peer = proxiedPeer;
|
|
351
|
+
peerInfo.isConnected = true;
|
|
352
|
+
// Add back to availablePeers
|
|
353
|
+
FullNodePeer.availablePeers.push(ip);
|
|
354
|
+
}
|
|
311
355
|
}
|
|
312
|
-
throw new Error(`Unable to connect to peer ${selectedPeerIP}`);
|
|
313
356
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
address: selectedPeerIP,
|
|
321
|
-
});
|
|
357
|
+
if (FullNodePeer.availablePeers.length === 0) {
|
|
358
|
+
throw new Error("No available peers to connect.");
|
|
359
|
+
}
|
|
360
|
+
// Select the next peer in round-robin
|
|
361
|
+
const selectedPeerIP = FullNodePeer.getNextPeerIP();
|
|
362
|
+
const selectedPeerInfo = FullNodePeer.peerInfos.get(selectedPeerIP);
|
|
322
363
|
// Cache the peer
|
|
323
|
-
FullNodePeer.cachedPeer = { peer:
|
|
364
|
+
FullNodePeer.cachedPeer = { peer: selectedPeerInfo.peer, timestamp: now };
|
|
324
365
|
console.log(`Using Fullnode Peer: ${selectedPeerIP}`);
|
|
325
|
-
return
|
|
366
|
+
return selectedPeerInfo.peer;
|
|
326
367
|
}
|
|
327
368
|
/**
|
|
328
|
-
* Creates a proxy for the peer to handle errors and
|
|
369
|
+
* Creates a proxy for the peer to handle errors, implement retries, and enforce per-peer throttling.
|
|
329
370
|
* @param {Peer} peer - The Peer instance.
|
|
330
371
|
* @param {string} peerIP - The IP address of the peer.
|
|
331
372
|
* @param {number} [retryCount=0] - The current retry attempt.
|
|
332
373
|
* @returns {Peer} The proxied Peer instance.
|
|
333
374
|
*/
|
|
334
375
|
static createPeerProxy(peer, peerIP, retryCount = 0) {
|
|
376
|
+
// Listen for close events if the Peer class supports it
|
|
377
|
+
// This assumes that the Peer class emits a 'close' event when the connection is closed
|
|
378
|
+
// Adjust accordingly based on the actual Peer implementation
|
|
379
|
+
if (typeof peer.on === "function") {
|
|
380
|
+
peer.on("close", () => {
|
|
381
|
+
console.warn(`Peer ${peerIP} connection closed.`);
|
|
382
|
+
FullNodePeer.handlePeerDisconnection(peerIP);
|
|
383
|
+
});
|
|
384
|
+
peer.on("error", (error) => {
|
|
385
|
+
console.error(`Peer ${peerIP} encountered an error: ${error.message}`);
|
|
386
|
+
FullNodePeer.handlePeerDisconnection(peerIP);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
335
389
|
return new Proxy(peer, {
|
|
336
390
|
get: (target, prop) => {
|
|
337
391
|
const originalMethod = target[prop];
|
|
338
392
|
if (typeof originalMethod === "function") {
|
|
339
|
-
return
|
|
393
|
+
return (...args) => {
|
|
394
|
+
// Select the next peer in round-robin
|
|
395
|
+
let selectedPeerIP;
|
|
340
396
|
try {
|
|
341
|
-
|
|
342
|
-
// On successful operation, increase the weight slightly
|
|
343
|
-
const currentWeight = FullNodePeer.peerWeights.get(peerIP) || 1;
|
|
344
|
-
FullNodePeer.peerWeights.set(peerIP, currentWeight + 0.1); // Increment weight
|
|
345
|
-
return result;
|
|
397
|
+
selectedPeerIP = FullNodePeer.getNextPeerIP();
|
|
346
398
|
}
|
|
347
399
|
catch (error) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
400
|
+
return Promise.reject(error);
|
|
401
|
+
}
|
|
402
|
+
const selectedPeerInfo = FullNodePeer.peerInfos.get(selectedPeerIP);
|
|
403
|
+
// Schedule the method call via the selected peer's limiter
|
|
404
|
+
return selectedPeerInfo.limiter.schedule(async () => {
|
|
405
|
+
const peerInfo = FullNodePeer.peerInfos.get(selectedPeerIP);
|
|
406
|
+
if (!peerInfo || !peerInfo.isConnected) {
|
|
407
|
+
throw new Error(`Cannot perform operation: Peer ${selectedPeerIP} is disconnected.`);
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const result = await originalMethod.apply(peerInfo.peer, args);
|
|
411
|
+
// On successful operation, increase the weight slightly
|
|
412
|
+
const currentWeight = FullNodePeer.peerWeights.get(selectedPeerIP) || 1;
|
|
413
|
+
FullNodePeer.peerWeights.set(selectedPeerIP, currentWeight + 0.1); // Increment weight
|
|
414
|
+
return result;
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
console.error(`Peer ${selectedPeerIP} encountered an error: ${error.message}`);
|
|
418
|
+
// Check if the error is related to WebSocket or Operation timed out
|
|
419
|
+
if (error.message.includes("WebSocket") ||
|
|
420
|
+
error.message.includes("Operation timed out")) {
|
|
421
|
+
// Handle the disconnection and mark the peer accordingly
|
|
422
|
+
FullNodePeer.handlePeerDisconnection(selectedPeerIP);
|
|
423
|
+
// If maximum retries reached, throw the error
|
|
424
|
+
if (retryCount >= MAX_RETRIES) {
|
|
425
|
+
console.error(`Max retries reached for method ${String(prop)} on peer ${selectedPeerIP}.`);
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
// Attempt to select a new peer and retry the method
|
|
429
|
+
try {
|
|
430
|
+
console.info(`Selecting a new peer to retry method ${String(prop)}...`);
|
|
431
|
+
const newPeer = await FullNodePeer.getBestPeer();
|
|
432
|
+
// Extract new peer's IP address
|
|
433
|
+
const newPeerIP = FullNodePeer.extractPeerIP(newPeer);
|
|
434
|
+
if (!newPeerIP) {
|
|
435
|
+
throw new Error("Unable to extract IP from the new peer.");
|
|
436
|
+
}
|
|
437
|
+
// Wrap the new peer with a proxy, incrementing the retry count
|
|
438
|
+
const proxiedNewPeer = FullNodePeer.createPeerProxy(newPeer, newPeerIP, retryCount + 1);
|
|
439
|
+
// Retry the method on the new peer
|
|
440
|
+
return await proxiedNewPeer[prop](...args);
|
|
441
|
+
}
|
|
442
|
+
catch (retryError) {
|
|
443
|
+
console.error(`Retry failed on a new peer: ${retryError.message}`);
|
|
444
|
+
throw retryError;
|
|
445
|
+
}
|
|
358
446
|
}
|
|
359
447
|
else {
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
// If maximum retries reached, throw the error
|
|
363
|
-
if (retryCount >= MAX_RETRIES) {
|
|
364
|
-
console.error(`Max retries reached for method ${String(prop)} on peer ${peerIP}.`);
|
|
448
|
+
// For other errors, handle normally
|
|
365
449
|
throw error;
|
|
366
450
|
}
|
|
367
|
-
// Attempt to select a new peer and retry the method
|
|
368
|
-
try {
|
|
369
|
-
console.info(`Selecting a new peer to retry method ${String(prop)}...`);
|
|
370
|
-
const newPeer = await FullNodePeer.getBestPeer();
|
|
371
|
-
// Extract new peer's IP address
|
|
372
|
-
const newPeerIP = FullNodePeer.extractPeerIP(newPeer);
|
|
373
|
-
if (!newPeerIP) {
|
|
374
|
-
throw new Error("Unable to extract IP from the new peer.");
|
|
375
|
-
}
|
|
376
|
-
// Wrap the new peer with a proxy, incrementing the retry count
|
|
377
|
-
const proxiedNewPeer = FullNodePeer.createPeerProxy(newPeer, newPeerIP, retryCount + 1);
|
|
378
|
-
// Retry the method on the new peer
|
|
379
|
-
return await proxiedNewPeer[prop](...args);
|
|
380
|
-
}
|
|
381
|
-
catch (retryError) {
|
|
382
|
-
console.error(`Retry failed on a new peer: ${retryError.message}`);
|
|
383
|
-
throw retryError;
|
|
384
|
-
}
|
|
385
451
|
}
|
|
386
|
-
|
|
387
|
-
// For other errors, handle normally
|
|
388
|
-
throw error;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
452
|
+
});
|
|
391
453
|
};
|
|
392
454
|
}
|
|
393
455
|
return originalMethod;
|
|
394
456
|
},
|
|
395
457
|
});
|
|
396
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* Handles peer disconnection by marking it in cooldown and updating internal states.
|
|
461
|
+
* @param {string} peerIP - The IP address of the disconnected peer.
|
|
462
|
+
*/
|
|
463
|
+
static handlePeerDisconnection(peerIP) {
|
|
464
|
+
// Mark the peer in cooldown
|
|
465
|
+
FullNodePeer.cooldownCache.set(peerIP, true);
|
|
466
|
+
// Decrease weight or remove peer
|
|
467
|
+
const currentWeight = FullNodePeer.peerWeights.get(peerIP) || 1;
|
|
468
|
+
if (currentWeight > 1) {
|
|
469
|
+
FullNodePeer.peerWeights.set(peerIP, currentWeight - 1);
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
FullNodePeer.peerWeights.delete(peerIP);
|
|
473
|
+
}
|
|
474
|
+
// Update the peer's connection status
|
|
475
|
+
const peerInfo = FullNodePeer.peerInfos.get(peerIP);
|
|
476
|
+
if (peerInfo) {
|
|
477
|
+
peerInfo.isConnected = false;
|
|
478
|
+
FullNodePeer.peerInfos.set(peerIP, peerInfo);
|
|
479
|
+
}
|
|
480
|
+
// Remove from availablePeers if present
|
|
481
|
+
const index = FullNodePeer.availablePeers.indexOf(peerIP);
|
|
482
|
+
if (index !== -1) {
|
|
483
|
+
FullNodePeer.availablePeers.splice(index, 1);
|
|
484
|
+
// Adjust currentPeerIndex if necessary
|
|
485
|
+
if (FullNodePeer.currentPeerIndex >= FullNodePeer.availablePeers.length) {
|
|
486
|
+
FullNodePeer.currentPeerIndex = 0;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// If the disconnected peer was the cached peer, invalidate the cache
|
|
490
|
+
if (FullNodePeer.cachedPeer &&
|
|
491
|
+
FullNodePeer.extractPeerIP(FullNodePeer.cachedPeer.peer) === peerIP) {
|
|
492
|
+
FullNodePeer.cachedPeer = null;
|
|
493
|
+
}
|
|
494
|
+
console.warn(`Peer ${peerIP} has been marked as disconnected and is in cooldown.`);
|
|
495
|
+
}
|
|
397
496
|
/**
|
|
398
497
|
* Extracts the IP address from a Peer instance.
|
|
399
498
|
* @param {Peer} peer - The Peer instance.
|
|
@@ -408,13 +507,21 @@ class FullNodePeer {
|
|
|
408
507
|
return null;
|
|
409
508
|
}
|
|
410
509
|
/**
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
510
|
+
* Waits for a coin to be confirmed (spent) on the blockchain.
|
|
511
|
+
* @param {Buffer} parentCoinInfo - The parent coin information.
|
|
512
|
+
* @returns {Promise<boolean>} Whether the coin was confirmed.
|
|
513
|
+
*/
|
|
415
514
|
static async waitForConfirmation(parentCoinInfo) {
|
|
416
515
|
const spinner = (0, nanospinner_1.createSpinner)("Waiting for confirmation...").start();
|
|
417
|
-
|
|
516
|
+
let peer;
|
|
517
|
+
try {
|
|
518
|
+
peer = await FullNodePeer.connect();
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
spinner.error({ text: "Failed to connect to a peer." });
|
|
522
|
+
console.error(`waitForConfirmation connection error: ${error.message}`);
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
418
525
|
try {
|
|
419
526
|
while (true) {
|
|
420
527
|
const confirmed = await peer.isCoinSpent(parentCoinInfo, config_1.MIN_HEIGHT, Buffer.from(config_1.MIN_HEIGHT_HEADER_HASH, "hex"));
|
|
@@ -422,7 +529,7 @@ class FullNodePeer {
|
|
|
422
529
|
spinner.success({ text: "Coin confirmed!" });
|
|
423
530
|
return true;
|
|
424
531
|
}
|
|
425
|
-
await
|
|
532
|
+
await FullNodePeer.delay(5000);
|
|
426
533
|
}
|
|
427
534
|
}
|
|
428
535
|
catch (error) {
|
|
@@ -431,6 +538,14 @@ class FullNodePeer {
|
|
|
431
538
|
throw error;
|
|
432
539
|
}
|
|
433
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* Delays execution for a specified amount of time.
|
|
543
|
+
* @param {number} ms - Milliseconds to delay.
|
|
544
|
+
* @returns {Promise<void>} A promise that resolves after the delay.
|
|
545
|
+
*/
|
|
546
|
+
static delay(ms) {
|
|
547
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
548
|
+
}
|
|
434
549
|
}
|
|
435
550
|
exports.FullNodePeer = FullNodePeer;
|
|
436
551
|
// Singleton instance
|
|
@@ -447,3 +562,7 @@ FullNodePeer.prioritizedPeers = [];
|
|
|
447
562
|
FullNodePeer.peerInfos = new Map();
|
|
448
563
|
// Cache for fetched peer IPs
|
|
449
564
|
FullNodePeer.peerIPCache = new node_cache_1.default({ stdTTL: CACHE_DURATION / 1000 });
|
|
565
|
+
// List of available peers for round-robin
|
|
566
|
+
FullNodePeer.availablePeers = [];
|
|
567
|
+
// Current index for round-robin selection
|
|
568
|
+
FullNodePeer.currentPeerIndex = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dignetwork/dig-sdk",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.136",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"archiver": "^7.0.1",
|
|
31
31
|
"axios": "^1.7.7",
|
|
32
32
|
"bip39": "^3.1.0",
|
|
33
|
+
"bottleneck": "^2.19.5",
|
|
33
34
|
"chia-bls": "^1.0.2",
|
|
34
35
|
"chia-config-loader": "^1.0.1",
|
|
35
36
|
"chia-root-resolver": "^1.0.0",
|