@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.
@@ -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;IA8EjD;;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"}
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
- if (response.status === 200) {
145
- console.log((0, colorette_1.green)(`✔ Peer was up to date: ${this.ipAddress}`));
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 storIds = storeFolders.filter((folder) => /^[a-f0-9]{64}$/.test(folder) &&
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 storIds.map((storeId) => DataStore.from(storeId));
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 a peer based on weighted random selection.
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 selectPeerByWeight;
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 weighted selection and reliability.
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 implement retries.
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
- * Waits for a coin to be confirmed (spent) on the blockchain.
104
- * @param {Buffer} parentCoinInfo - The parent coin information.
105
- * @returns {Promise<boolean>} Whether the coin was confirmed.
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;AAiCzD;;GAEG;AACH,qBAAa,YAAY;IAuBH,OAAO,CAAC,IAAI;IArBhC,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;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;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAwBjC;;;OAGG;mBACkB,UAAU;IAK/B;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,UAAU;IAIzB;;;OAGG;mBACkB,WAAW;IAyEhC;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe;IAuE9B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IAS1B;;;;KAIC;WACmB,mBAAmB,CACrC,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,OAAO,CAAC;CAyBtB"}
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 = 60000; // in milliseconds
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 a peer based on weighted random selection.
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 selectPeerByWeight() {
232
- const peers = Array.from(FullNodePeer.peerWeights.entries())
233
- .filter(([ip, _]) => !FullNodePeer.cooldownCache.has(ip))
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 random = Math.random() * totalWeight;
240
- let cumulative = 0;
241
- for (const peer of peers) {
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 weighted selection and reliability.
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
- // Weighted random selection
281
- let selectedPeerIP;
282
- try {
283
- selectedPeerIP = FullNodePeer.selectPeerByWeight();
284
- }
285
- catch (error) {
286
- throw new Error(`Failed to select a peer: ${error.message}`);
287
- }
288
- // Attempt to create a peer connection
289
- const sslFolder = path_1.default.resolve(os_1.default.homedir(), ".dig", "ssl");
290
- const certFile = path_1.default.join(sslFolder, "public_dig.crt");
291
- const keyFile = path_1.default.join(sslFolder, "public_dig.key");
292
- if (!fs_1.default.existsSync(sslFolder)) {
293
- fs_1.default.mkdirSync(sslFolder, { recursive: true });
294
- }
295
- const tls = new datalayer_driver_1.Tls(certFile, keyFile);
296
- let peer;
297
- try {
298
- peer = await datalayer_driver_1.Peer.new(`${selectedPeerIP}:${FULLNODE_PORT}`, false, tls);
299
- }
300
- catch (error) {
301
- console.error(`Failed to create peer for IP ${selectedPeerIP}: ${error.message}`);
302
- // Add to cooldown
303
- FullNodePeer.cooldownCache.set(selectedPeerIP, true);
304
- // Decrease weight or remove peer
305
- const currentWeight = FullNodePeer.peerWeights.get(selectedPeerIP) || 1;
306
- if (currentWeight > 1) {
307
- FullNodePeer.peerWeights.set(selectedPeerIP, currentWeight - 1);
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.peerWeights.delete(selectedPeerIP);
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
- // Wrap the peer with proxy to handle errors and retries
315
- const proxiedPeer = FullNodePeer.createPeerProxy(peer, selectedPeerIP);
316
- // Store PeerInfo
317
- FullNodePeer.peerInfos.set(selectedPeerIP, {
318
- peer: proxiedPeer,
319
- weight: FullNodePeer.peerWeights.get(selectedPeerIP) || 1,
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: proxiedPeer, timestamp: now };
364
+ FullNodePeer.cachedPeer = { peer: selectedPeerInfo.peer, timestamp: now };
324
365
  console.log(`Using Fullnode Peer: ${selectedPeerIP}`);
325
- return proxiedPeer;
366
+ return selectedPeerInfo.peer;
326
367
  }
327
368
  /**
328
- * Creates a proxy for the peer to handle errors and implement retries.
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 async (...args) => {
393
+ return (...args) => {
394
+ // Select the next peer in round-robin
395
+ let selectedPeerIP;
340
396
  try {
341
- const result = await originalMethod.apply(target, args);
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
- console.error(`Peer ${peerIP} encountered an error: ${error.message}`);
349
- // Check if the error is related to WebSocket or Operation timed out
350
- if (error.message.includes("WebSocket") ||
351
- error.message.includes("Operation timed out")) {
352
- // Add the faulty peer to the cooldown cache
353
- FullNodePeer.cooldownCache.set(peerIP, true);
354
- // Decrease weight or remove peer
355
- const currentWeight = FullNodePeer.peerWeights.get(peerIP) || 1;
356
- if (currentWeight > 1) {
357
- FullNodePeer.peerWeights.set(peerIP, currentWeight - 1);
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
- FullNodePeer.peerWeights.delete(peerIP);
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
- else {
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
- * Waits for a coin to be confirmed (spent) on the blockchain.
412
- * @param {Buffer} parentCoinInfo - The parent coin information.
413
- * @returns {Promise<boolean>} Whether the coin was confirmed.
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
- const peer = await FullNodePeer.connect();
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 new Promise((resolve) => setTimeout(resolve, 5000));
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.134",
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",