@pioneer-platform/blockbook 8.3.15 → 8.3.17

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/lib/index.js CHANGED
@@ -1,8 +1,23 @@
1
1
  "use strict";
2
- /*
3
-
4
-
2
+ /**
3
+ * Blockbook Client Integration Module
4
+ *
5
+ * Provides a unified interface for interacting with Blockbook blockchain explorers
6
+ * across multiple cryptocurrency networks. Handles pagination, retries, and fallbacks.
7
+ *
8
+ * @module blockbook-client
5
9
  */
10
+ var __assign = (this && this.__assign) || function () {
11
+ __assign = Object.assign || function(t) {
12
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
13
+ s = arguments[i];
14
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
15
+ t[p] = s[p];
16
+ }
17
+ return t;
18
+ };
19
+ return __assign.apply(this, arguments);
20
+ };
6
21
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
7
22
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
8
23
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -39,134 +54,184 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
39
54
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
40
55
  }
41
56
  };
42
- var TAG = " | blockbook-client | ";
57
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
58
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
59
+ if (ar || !(i in from)) {
60
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
61
+ ar[i] = from[i];
62
+ }
63
+ }
64
+ return to.concat(ar || Array.prototype.slice.call(from));
65
+ };
43
66
  var Blockbook = require('blockbook-client').Blockbook;
44
67
  var log = require('@pioneer-platform/loggerdog')();
45
68
  var fakeUa = require('fake-useragent');
46
69
  var Axios = require('axios');
47
70
  var https = require('https');
48
- var nodes = require("@pioneer-platform/nodes");
71
+ var nodes = require('@pioneer-platform/nodes');
72
+ var axiosRetry = require('axios-retry');
73
+ var TAG = " | blockbook-client | ";
74
+ var NOW_NODES_API = process.env['NOW_NODES_API'];
75
+ // Configure axios with retry logic and custom settings
49
76
  var axios = Axios.create({
50
77
  httpsAgent: new https.Agent({
51
78
  rejectUnauthorized: false
52
79
  }),
53
- timeout: 30000 // 10 seconds
80
+ timeout: 30000 // 30 seconds timeout
54
81
  });
55
- // const axiosRetry = require('axios-retry');
56
- var NOW_NODES_API = process.env['NOW_NODES_API'];
57
- // axiosRetry(axios, {
58
- // retries: 3, // number of retries
59
- // retryDelay: (retryCount: number) => {
60
- // log.error(TAG,`retry attempt: ${retryCount}`);
61
- // return retryCount * 2000; // time interval between retries
62
- // },
63
- // retryCondition: (error: { response: { status: number; }; }) => {
64
- // log.error(TAG,error)
65
- // //@TODO mark node offline, and punish
66
- // // if retry condition is not specified, by default idempotent requests are retried
67
- // return error?.response?.status === 503;
68
- // },
69
- // });
82
+ // Configure automatic retries for transient failures
83
+ axiosRetry(axios, {
84
+ retries: 3,
85
+ retryDelay: function (retryCount) {
86
+ log.error(TAG, "Retry attempt: ".concat(retryCount));
87
+ return retryCount * 2000; // Exponential backoff
88
+ },
89
+ retryCondition: function (error) {
90
+ var _a;
91
+ // Retry on network errors or 5xx status codes
92
+ return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
93
+ (((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) ? error.response.status >= 500 : false);
94
+ },
95
+ });
96
+ // Global storage for Blockbook URLs and WebSocket connections
70
97
  var BLOCKBOOK_URLS = {};
71
98
  var BLOCKBOOK_SOCKETS = {};
99
+ // Cache for frequently accessed data
100
+ var cache = new Map();
101
+ var CACHE_TTL = 60000; // 1 minute cache TTL
102
+ /**
103
+ * Get cached data if available and not expired
104
+ */
105
+ function getCached(key) {
106
+ var cached = cache.get(key);
107
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
108
+ return cached.data;
109
+ }
110
+ cache.delete(key);
111
+ return null;
112
+ }
113
+ /**
114
+ * Store data in cache
115
+ */
116
+ function setCache(key, data) {
117
+ cache.set(key, { data: data, timestamp: Date.now() });
118
+ }
119
+ // Main module exports
72
120
  module.exports = {
73
- init: function (servers) {
74
- return init_network(servers);
75
- },
76
- getInfo: function () {
77
- return get_node_info();
78
- },
79
- getBlockbooks: function () {
80
- return BLOCKBOOK_URLS;
81
- },
82
- getBlockbookSockets: function () {
83
- return BLOCKBOOK_SOCKETS;
84
- },
85
- getFees: function (coin) {
86
- return get_fees(coin);
87
- },
88
- getTransaction: function (coin, txid) {
89
- return get_transaction(coin, txid);
90
- },
91
- getAddressInfo: function (coin, address, filter) {
92
- return get_info_by_address(coin, address, filter);
93
- },
94
- getPubkeyInfo: function (coin, pubkey, filter) {
95
- return get_info_by_pubkey(coin, pubkey, filter);
96
- },
97
- txidsByAddress: function (coin, address, page) {
98
- return get_txids_by_address(coin, address, page);
99
- },
100
- txsByXpub: function (coin, addresses) {
101
- return get_txs_by_xpub(coin, addresses);
102
- },
103
- utxosByXpub: function (coin, xpub) {
104
- return get_utxos_by_xpub(coin, xpub);
105
- },
106
- getBalanceByXpub: function (coin, xpub) {
107
- return get_balance_by_xpub(coin, xpub);
108
- },
109
- broadcast: function (coin, hex) {
110
- return broadcast_transaction(coin, hex);
111
- },
121
+ init: init_network,
122
+ getInfo: get_node_info,
123
+ getBlockbooks: function () { return BLOCKBOOK_URLS; },
124
+ getBlockbookSockets: function () { return BLOCKBOOK_SOCKETS; },
125
+ getFees: get_fees,
126
+ getTransaction: get_transaction,
127
+ getAddressInfo: get_info_by_address,
128
+ getPubkeyInfo: get_info_by_pubkey,
129
+ getNextIndexes: get_next_indexes_by_pubkey,
130
+ computeNextIndexes: computeNextIndexes,
131
+ txidsByAddress: get_txids_by_address,
132
+ txsByXpub: get_txs_by_xpub,
133
+ utxosByXpub: get_utxos_by_xpub,
134
+ getBalanceByXpub: get_balance_by_xpub,
135
+ broadcast: broadcast_transaction,
112
136
  };
113
- var init_network = function (servers) {
137
+ /**
138
+ * Initialize the Blockbook network with seed nodes and custom servers
139
+ * @param servers Optional array of custom Blockbook server configurations
140
+ * @returns Promise<boolean> indicating successful initialization
141
+ */
142
+ function init_network(servers) {
114
143
  return __awaiter(this, void 0, void 0, function () {
115
- var tag, SEED_NODES, blockbooks, i, blockbook, url, e_1;
144
+ var tag, SEED_NODES, allNodes, blockbooks, _i, blockbooks_1, blockbook, symbol, httpUrl, e_1;
116
145
  return __generator(this, function (_a) {
117
146
  switch (_a.label) {
118
147
  case 0:
119
- tag = ' | get_txs_by_address | ';
148
+ tag = TAG + " | init_network | ";
120
149
  _a.label = 1;
121
150
  case 1:
122
- _a.trys.push([1, 3, , 4]);
123
- log.debug(tag, "checkpoint: ");
151
+ _a.trys.push([1, 7, , 8]);
152
+ log.debug(tag, "Initializing Blockbook network...");
153
+ SEED_NODES = [];
154
+ if (!(typeof nodes.getBlockbooks === 'function')) return [3 /*break*/, 3];
124
155
  return [4 /*yield*/, nodes.getBlockbooks()];
125
156
  case 2:
126
157
  SEED_NODES = _a.sent();
127
- log.info(tag, "SEED_NODES: ", SEED_NODES);
158
+ return [3 /*break*/, 6];
159
+ case 3:
160
+ if (!(typeof nodes.getNodes === 'function')) return [3 /*break*/, 5];
161
+ return [4 /*yield*/, nodes.getNodes()];
162
+ case 4:
163
+ allNodes = _a.sent();
164
+ SEED_NODES = allNodes.filter(function (n) { return n.type === 'blockbook' || n.service; });
165
+ return [3 /*break*/, 6];
166
+ case 5:
167
+ if (nodes.blockbooks) {
168
+ SEED_NODES = nodes.blockbooks;
169
+ }
170
+ else {
171
+ log.warn(tag, "Unable to load seed nodes from @pioneer-platform/nodes");
172
+ }
173
+ _a.label = 6;
174
+ case 6:
175
+ log.info(tag, "Loaded ".concat(SEED_NODES.length, " seed nodes"));
128
176
  blockbooks = [];
129
- if (servers && Array.isArray(servers)) { // Type checking for array
130
- blockbooks = servers.concat(SEED_NODES); // Combine arrays
177
+ if (servers && Array.isArray(servers)) {
178
+ blockbooks = servers.concat(SEED_NODES);
179
+ log.info(tag, "Added ".concat(servers.length, " custom servers"));
131
180
  }
132
181
  else {
133
- console.error("Invalid 'servers' parameter. Expected an array.");
182
+ if (servers) {
183
+ log.warn(tag, "Invalid 'servers' parameter. Expected an array.");
184
+ }
134
185
  blockbooks = SEED_NODES;
135
186
  }
136
- log.debug(tag, "blockbooks: ", blockbooks.length);
137
- for (i = 0; i < blockbooks.length; i++) {
138
- blockbook = blockbooks[i];
139
- //get swagger
140
- if (blockbook && blockbook.service)
141
- BLOCKBOOK_URLS[blockbook.symbol.toUpperCase()] = blockbook.service;
142
- if (blockbook && blockbook.websocket) {
143
- url = blockbook.websocket.replace("/websocket", "");
144
- url = blockbook.websocket.replace("wss://", "https://");
145
- BLOCKBOOK_SOCKETS[blockbook.symbol.toUpperCase()] = new Blockbook({
146
- nodes: [url],
147
- disableTypeValidation: true,
148
- });
187
+ log.debug(tag, "Total blockbook servers: ".concat(blockbooks.length));
188
+ // Initialize each Blockbook server
189
+ for (_i = 0, blockbooks_1 = blockbooks; _i < blockbooks_1.length; _i++) {
190
+ blockbook = blockbooks_1[_i];
191
+ try {
192
+ if (blockbook === null || blockbook === void 0 ? void 0 : blockbook.service) {
193
+ symbol = blockbook.symbol.toUpperCase();
194
+ BLOCKBOOK_URLS[symbol] = blockbook.service;
195
+ if (blockbook.websocket) {
196
+ httpUrl = blockbook.websocket
197
+ .replace("/websocket", "")
198
+ .replace("wss://", "https://")
199
+ .replace("ws://", "http://");
200
+ BLOCKBOOK_SOCKETS[symbol] = new Blockbook({
201
+ nodes: [httpUrl],
202
+ disableTypeValidation: true,
203
+ });
204
+ log.debug(tag, "Initialized ".concat(symbol, " with URL: ").concat(blockbook.service));
205
+ }
206
+ }
207
+ else {
208
+ log.warn(tag, "Invalid blockbook configuration:", blockbook);
209
+ }
149
210
  }
150
- else {
151
- log.error(tag, "invalid unchained service: ", blockbook);
152
- // throw Error("invalid unchained service!")
211
+ catch (error) {
212
+ log.error(tag, "Failed to initialize blockbook ".concat(blockbook === null || blockbook === void 0 ? void 0 : blockbook.symbol, ":"), error);
153
213
  }
154
214
  }
155
- log.debug(tag, "BLOCKBOOK_URLS: ", BLOCKBOOK_URLS);
156
- log.debug(tag, "BLOCKBOOK_SOCKETS: ", BLOCKBOOK_SOCKETS);
215
+ log.info(tag, "Initialized ".concat(Object.keys(BLOCKBOOK_URLS).length, " Blockbook URLs"));
216
+ log.info(tag, "Initialized ".concat(Object.keys(BLOCKBOOK_SOCKETS).length, " WebSocket connections"));
157
217
  return [2 /*return*/, true];
158
- case 3:
218
+ case 7:
159
219
  e_1 = _a.sent();
160
- // console.error(tag, 'Error: ', e)
220
+ log.error(tag, "Failed to initialize network:", e_1);
161
221
  throw e_1;
162
- case 4: return [2 /*return*/];
222
+ case 8: return [2 /*return*/];
163
223
  }
164
224
  });
165
225
  });
166
- };
167
- var get_fees = function (coin) {
226
+ }
227
+ /**
228
+ * Get fee estimates for a specific coin
229
+ * @param coin Coin symbol (e.g., 'BTC', 'ETH')
230
+ * @returns Promise<FeeEstimate> containing fee recommendations
231
+ */
232
+ function get_fees(coin) {
168
233
  return __awaiter(this, void 0, void 0, function () {
169
- var tag, url, body, resp, e_2;
234
+ var tag, cacheKey, cached, baseUrl, url, resp, feeData, e_2;
170
235
  return __generator(this, function (_a) {
171
236
  switch (_a.label) {
172
237
  case 0:
@@ -174,74 +239,254 @@ var get_fees = function (coin) {
174
239
  _a.label = 1;
175
240
  case 1:
176
241
  _a.trys.push([1, 3, , 4]);
177
- url = BLOCKBOOK_URLS[coin.toUpperCase()] + "/api/v2/fees";
178
- log.debug(tag, "url: ", url);
179
- body = {
180
- method: 'GET',
181
- url: url,
182
- headers: {
183
- 'content-type': 'application/json',
184
- 'User-Agent': fakeUa()
185
- },
186
- };
187
- return [4 /*yield*/, axios(body)
188
- //log.debug(tag,"resp: ",resp)
189
- //TODO paginate?
190
- ];
242
+ cacheKey = "fees:".concat(coin.toUpperCase());
243
+ cached = getCached(cacheKey);
244
+ if (cached) {
245
+ log.debug(tag, "Returning cached fees for ".concat(coin));
246
+ return [2 /*return*/, cached];
247
+ }
248
+ baseUrl = BLOCKBOOK_URLS[coin.toUpperCase()];
249
+ if (!baseUrl) {
250
+ throw new Error("Unknown coin '".concat(coin, "' or missing BLOCKBOOK_URLS entry"));
251
+ }
252
+ url = "".concat(baseUrl, "/api/v2/fees");
253
+ log.debug(tag, "Fetching fees from:", url);
254
+ return [4 /*yield*/, axios({
255
+ method: 'GET',
256
+ url: url,
257
+ headers: {
258
+ 'content-type': 'application/json',
259
+ 'User-Agent': typeof fakeUa === 'function' ? fakeUa() : 'blockbook-client/1.0'
260
+ },
261
+ })];
191
262
  case 2:
192
263
  resp = _a.sent();
193
- //log.debug(tag,"resp: ",resp)
194
- //TODO paginate?
195
- return [2 /*return*/, resp.data];
264
+ feeData = resp.data;
265
+ setCache(cacheKey, feeData);
266
+ return [2 /*return*/, feeData];
196
267
  case 3:
197
268
  e_2 = _a.sent();
198
- console.error(tag, e_2);
269
+ log.error(tag, "Failed to get fees for ".concat(coin, ":"), e_2);
199
270
  throw e_2;
200
271
  case 4: return [2 /*return*/];
201
272
  }
202
273
  });
203
274
  });
204
- };
205
- var get_info_by_pubkey = function (coin, pubkey, page) {
206
- return __awaiter(this, void 0, void 0, function () {
207
- var tag, url, body, resp, e_3;
208
- return __generator(this, function (_a) {
209
- switch (_a.label) {
275
+ }
276
+ /**
277
+ * Fetch xpub info from Blockbook with tokens, handling pagination and fallbacks.
278
+ * @param coin Uppercase/anycase coin key resolvable in BLOCKBOOK_URLS (e.g., 'BTC')
279
+ * @param pubkey xpub/ypub/zpub/tpub/vpub
280
+ * @param page Starting page number (default 1)
281
+ * @param pageSize Items per page (default 1000)
282
+ * @returns Promise<XpubResponse> with complete token information including changeIndex and receiveIndex
283
+ */
284
+ function get_info_by_pubkey(coin_1, pubkey_1) {
285
+ return __awaiter(this, arguments, void 0, function (coin, pubkey, page, pageSize) {
286
+ var tag, baseUrl, url_1, fetchPage, params, data, totalPages, allTokens, startPage, pagePromises, p, pageResults, _i, pageResults_1, pageData, txidsParams, txidsData, err_1, nextIndexes, enhancedData, e_3;
287
+ var _this = this;
288
+ var _a, _b, _c, _d;
289
+ if (page === void 0) { page = 1; }
290
+ if (pageSize === void 0) { pageSize = 1000; }
291
+ return __generator(this, function (_e) {
292
+ switch (_e.label) {
210
293
  case 0:
211
294
  tag = TAG + " | get_info_by_pubkey | ";
212
- _a.label = 1;
295
+ _e.label = 1;
213
296
  case 1:
214
- _a.trys.push([1, 3, , 4]);
215
- if (!page)
216
- page = "1";
217
- url = BLOCKBOOK_URLS[coin.toUpperCase()] + "/api/v2/xpub/" + pubkey;
218
- log.debug(tag, "url: ", url);
219
- body = {
220
- method: 'GET',
221
- url: url,
222
- headers: {
223
- 'content-type': 'application/json',
224
- 'User-Agent': fakeUa()
225
- },
297
+ _e.trys.push([1, 11, , 12]);
298
+ baseUrl = BLOCKBOOK_URLS[coin.toUpperCase()];
299
+ if (!baseUrl) {
300
+ throw new Error("Unknown coin '".concat(coin, "' or missing BLOCKBOOK_URLS entry"));
301
+ }
302
+ url_1 = "".concat(baseUrl, "/api/v2/xpub/").concat(encodeURIComponent(pubkey));
303
+ fetchPage = function (params) { return __awaiter(_this, void 0, void 0, function () {
304
+ var resp;
305
+ return __generator(this, function (_a) {
306
+ switch (_a.label) {
307
+ case 0:
308
+ log.debug(tag, "Fetching page with params:", params);
309
+ return [4 /*yield*/, axios({
310
+ method: "GET",
311
+ url: url_1,
312
+ headers: {
313
+ "content-type": "application/json",
314
+ "User-Agent": typeof fakeUa === "function" ? fakeUa() : "blockbook-client/1.0",
315
+ },
316
+ params: params,
317
+ })];
318
+ case 1:
319
+ resp = _a.sent();
320
+ return [2 /*return*/, resp.data];
321
+ }
322
+ });
323
+ }); };
324
+ params = {
325
+ page: page,
326
+ pageSize: pageSize,
327
+ details: "tokenBalances",
328
+ tokens: "derived",
226
329
  };
227
- return [4 /*yield*/, axios(body)];
330
+ return [4 /*yield*/, fetchPage(params)];
228
331
  case 2:
229
- resp = _a.sent();
230
- log.debug(tag, "resp: ", resp);
231
- //TODO paginate?
232
- return [2 /*return*/, resp.data];
332
+ data = _e.sent();
333
+ log.debug(tag, "Initial fetch returned ".concat(((_a = data.tokens) === null || _a === void 0 ? void 0 : _a.length) || 0, " tokens"));
334
+ if (!(!data.tokens || data.tokens.length === 0)) return [3 /*break*/, 4];
335
+ log.debug(tag, "No derived tokens found, trying 'used' fallback");
336
+ params = {
337
+ page: page,
338
+ pageSize: pageSize,
339
+ details: "tokens",
340
+ tokens: "used",
341
+ };
342
+ return [4 /*yield*/, fetchPage(params)];
233
343
  case 3:
234
- e_3 = _a.sent();
235
- console.error(tag, e_3);
344
+ data = _e.sent();
345
+ _e.label = 4;
346
+ case 4:
347
+ // Ensure tokens array exists
348
+ if (!data.tokens) {
349
+ data.tokens = [];
350
+ }
351
+ totalPages = Number((_b = data.totalPages) !== null && _b !== void 0 ? _b : 1);
352
+ if (!(totalPages > 1)) return [3 /*break*/, 6];
353
+ log.info(tag, "Fetching ".concat(totalPages, " pages of token data"));
354
+ allTokens = __spreadArray([], data.tokens, true);
355
+ startPage = Number((_c = data.page) !== null && _c !== void 0 ? _c : page);
356
+ pagePromises = [];
357
+ for (p = startPage + 1; p <= totalPages; p++) {
358
+ pagePromises.push(fetchPage(__assign(__assign({}, params), { page: p })));
359
+ }
360
+ return [4 /*yield*/, Promise.all(pagePromises)];
361
+ case 5:
362
+ pageResults = _e.sent();
363
+ for (_i = 0, pageResults_1 = pageResults; _i < pageResults_1.length; _i++) {
364
+ pageData = pageResults_1[_i];
365
+ if ((_d = pageData === null || pageData === void 0 ? void 0 : pageData.tokens) === null || _d === void 0 ? void 0 : _d.length) {
366
+ allTokens.push.apply(allTokens, pageData.tokens);
367
+ }
368
+ }
369
+ // Update metadata to reflect merged data
370
+ data.tokens = allTokens;
371
+ data.page = 1;
372
+ data.totalPages = 1;
373
+ data.itemsOnPage = allTokens.length;
374
+ log.info(tag, "Merged ".concat(allTokens.length, " total tokens from ").concat(totalPages, " pages"));
375
+ _e.label = 6;
376
+ case 6:
377
+ if (!(data.txs > 0 && (!data.txids || data.txids.length === 0))) return [3 /*break*/, 10];
378
+ log.debug(tag, "Fetching txids for ".concat(data.txs, " transactions"));
379
+ txidsParams = {
380
+ page: 1,
381
+ pageSize: Math.min(1000, data.txs * 2), // Get more to ensure we get all
382
+ details: "txids",
383
+ };
384
+ _e.label = 7;
385
+ case 7:
386
+ _e.trys.push([7, 9, , 10]);
387
+ return [4 /*yield*/, fetchPage(txidsParams)];
388
+ case 8:
389
+ txidsData = _e.sent();
390
+ if (txidsData.txids && txidsData.txids.length > 0) {
391
+ data.txids = txidsData.txids;
392
+ log.info(tag, "Fetched ".concat(txidsData.txids.length, " txids"));
393
+ }
394
+ return [3 /*break*/, 10];
395
+ case 9:
396
+ err_1 = _e.sent();
397
+ log.warn(tag, "Failed to fetch txids separately:", err_1);
398
+ return [3 /*break*/, 10];
399
+ case 10:
400
+ nextIndexes = computeNextIndexes(data.tokens || []);
401
+ enhancedData = __assign(__assign({}, data), { changeIndex: nextIndexes.nextChangeAddressIndex, receiveIndex: nextIndexes.nextReceiveAddressIndex });
402
+ return [2 /*return*/, enhancedData];
403
+ case 11:
404
+ e_3 = _e.sent();
405
+ log.error(tag, "Failed to get pubkey info for ".concat(coin, ":"), e_3);
236
406
  throw e_3;
237
- case 4: return [2 /*return*/];
407
+ case 12: return [2 /*return*/];
238
408
  }
239
409
  });
240
410
  });
241
- };
242
- var get_txids_by_address = function (coin, address, page) {
243
- return __awaiter(this, void 0, void 0, function () {
244
- var tag, url, body, resp, e_4;
411
+ }
412
+ /**
413
+ * Compute next indexes from Blockbook tokens array.
414
+ * Returns the next external (receive) and internal (change) indexes.
415
+ *
416
+ * Logic:
417
+ * - Consider an address "used" if transfers > 0 OR balance !== '0'
418
+ * - Next index = (max used index on that branch) + 1
419
+ *
420
+ * @param tokens Array of BlockbookToken objects with path information
421
+ * @returns Object with nextReceiveAddressIndex and nextChangeAddressIndex
422
+ */
423
+ function computeNextIndexes(tokens) {
424
+ var _a, _b, _c, _d;
425
+ var next = []; // next[0] = next receive, next[1] = next change
426
+ for (var _i = 0, _e = tokens !== null && tokens !== void 0 ? tokens : []; _i < _e.length; _i++) {
427
+ var token = _e[_i];
428
+ if (!token.path)
429
+ continue;
430
+ // Parse BIP32 path: m/84'/0'/0'/1/5
431
+ var parts = token.path.split("/");
432
+ if (parts.length < 6)
433
+ continue;
434
+ var change = Number(parts[4]);
435
+ var index = Number(parts[5]);
436
+ if (!Number.isInteger(change) || !Number.isInteger(index))
437
+ continue;
438
+ if (change !== 0 && change !== 1)
439
+ continue; // Only handle standard change values
440
+ // Check if address is used
441
+ var used = ((_a = token.transfers) !== null && _a !== void 0 ? _a : 0) > 0 || ((_b = token.balance) !== null && _b !== void 0 ? _b : "0") !== "0";
442
+ if (!used)
443
+ continue;
444
+ // Update next index if this is higher than current
445
+ var candidate = index + 1;
446
+ if (next[change] == null || candidate > next[change]) {
447
+ next[change] = candidate;
448
+ }
449
+ }
450
+ return {
451
+ nextReceiveAddressIndex: (_c = next[0]) !== null && _c !== void 0 ? _c : 0,
452
+ nextChangeAddressIndex: (_d = next[1]) !== null && _d !== void 0 ? _d : 0,
453
+ };
454
+ }
455
+ /**
456
+ * Convenience function: fetch tokens and return the next indexes directly.
457
+ * @param coin Coin symbol
458
+ * @param pubkey Extended public key
459
+ * @param page Starting page
460
+ * @param pageSize Items per page
461
+ * @returns Promise<NextIndexes> with next receive and change indexes
462
+ */
463
+ function get_next_indexes_by_pubkey(coin_1, pubkey_1) {
464
+ return __awaiter(this, arguments, void 0, function (coin, pubkey, page, pageSize) {
465
+ var info;
466
+ var _a;
467
+ if (page === void 0) { page = 1; }
468
+ if (pageSize === void 0) { pageSize = 1000; }
469
+ return __generator(this, function (_b) {
470
+ switch (_b.label) {
471
+ case 0: return [4 /*yield*/, get_info_by_pubkey(coin, pubkey, page, pageSize)];
472
+ case 1:
473
+ info = _b.sent();
474
+ return [2 /*return*/, computeNextIndexes((_a = info.tokens) !== null && _a !== void 0 ? _a : [])];
475
+ }
476
+ });
477
+ });
478
+ }
479
+ /**
480
+ * Get transaction IDs for a specific address with pagination support
481
+ * @param coin Coin symbol
482
+ * @param address Blockchain address
483
+ * @param page Page number (default 1)
484
+ * @returns Promise with address info including transaction IDs
485
+ */
486
+ function get_txids_by_address(coin_1, address_1) {
487
+ return __awaiter(this, arguments, void 0, function (coin, address, page) {
488
+ var tag, baseUrl, url, resp, data, e_4;
489
+ if (page === void 0) { page = 1; }
245
490
  return __generator(this, function (_a) {
246
491
  switch (_a.label) {
247
492
  case 0:
@@ -249,37 +494,53 @@ var get_txids_by_address = function (coin, address, page) {
249
494
  _a.label = 1;
250
495
  case 1:
251
496
  _a.trys.push([1, 3, , 4]);
252
- if (!page)
253
- page = 1;
254
- url = BLOCKBOOK_URLS[coin.toUpperCase()] + "/api/v2/address/" + address + "?page=" + page + "&details=all";
255
- log.debug(tag, "url: ", url);
256
- body = {
257
- method: 'GET',
258
- url: url,
259
- headers: {
260
- 'content-type': 'application/json',
261
- 'User-Agent': fakeUa()
262
- },
263
- };
264
- return [4 /*yield*/, axios(body)
265
- //TODO paginate?
266
- ];
497
+ baseUrl = BLOCKBOOK_URLS[coin.toUpperCase()];
498
+ if (!baseUrl) {
499
+ throw new Error("Unknown coin '".concat(coin, "' or missing BLOCKBOOK_URLS entry"));
500
+ }
501
+ url = "".concat(baseUrl, "/api/v2/address/").concat(address);
502
+ log.debug(tag, "Fetching txids from:", url);
503
+ return [4 /*yield*/, axios({
504
+ method: 'GET',
505
+ url: url,
506
+ headers: {
507
+ 'content-type': 'application/json',
508
+ 'User-Agent': typeof fakeUa === 'function' ? fakeUa() : 'blockbook-client/1.0'
509
+ },
510
+ params: {
511
+ page: page,
512
+ details: 'txids',
513
+ pageSize: 1000
514
+ }
515
+ })];
267
516
  case 2:
268
517
  resp = _a.sent();
269
- //TODO paginate?
270
- return [2 /*return*/, resp.data];
518
+ data = resp.data;
519
+ if (data.totalPages > 1 && page === 1) {
520
+ log.info(tag, "Address has ".concat(data.totalPages, " pages of transactions"));
521
+ // Consider implementing full pagination here if needed
522
+ }
523
+ return [2 /*return*/, data];
271
524
  case 3:
272
525
  e_4 = _a.sent();
273
- console.error(tag, e_4);
526
+ log.error(tag, "Failed to get txids for address ".concat(address, ":"), e_4);
274
527
  throw e_4;
275
528
  case 4: return [2 /*return*/];
276
529
  }
277
530
  });
278
531
  });
279
- };
280
- var get_info_by_address = function (coin, address, filter) {
281
- return __awaiter(this, void 0, void 0, function () {
282
- var tag, url, body, resp, e_5;
532
+ }
533
+ /**
534
+ * Get detailed information for a specific address
535
+ * @param coin Coin symbol
536
+ * @param address Blockchain address
537
+ * @param filter Detail level filter (default 'all')
538
+ * @returns Promise with complete address information
539
+ */
540
+ function get_info_by_address(coin_1, address_1) {
541
+ return __awaiter(this, arguments, void 0, function (coin, address, filter) {
542
+ var tag, baseUrl, url, resp, e_5;
543
+ if (filter === void 0) { filter = 'all'; }
283
544
  return __generator(this, function (_a) {
284
545
  switch (_a.label) {
285
546
  case 0:
@@ -287,118 +548,141 @@ var get_info_by_address = function (coin, address, filter) {
287
548
  _a.label = 1;
288
549
  case 1:
289
550
  _a.trys.push([1, 3, , 4]);
290
- if (!filter)
291
- filter = "all";
292
- //let url = ETH_BLOCKBOOK_URL+"/api/v2/address/"+address+"?="+filter
293
- if (!BLOCKBOOK_URLS[coin.toUpperCase()])
294
- throw Error("invalid coin: " + coin);
295
- url = BLOCKBOOK_URLS[coin.toUpperCase()] + "/api/v2/address/" + address + "?details=all";
296
- body = {
297
- method: 'GET',
298
- url: url,
299
- headers: {
300
- 'content-type': 'application/json',
301
- 'User-Agent': fakeUa()
302
- },
303
- };
304
- return [4 /*yield*/, axios(body)
305
- //TODO paginate?
306
- ];
551
+ baseUrl = BLOCKBOOK_URLS[coin.toUpperCase()];
552
+ if (!baseUrl) {
553
+ throw new Error("Unknown coin '".concat(coin, "' or missing BLOCKBOOK_URLS entry"));
554
+ }
555
+ url = "".concat(baseUrl, "/api/v2/address/").concat(address);
556
+ log.debug(tag, "Fetching address info from:", url);
557
+ return [4 /*yield*/, axios({
558
+ method: 'GET',
559
+ url: url,
560
+ headers: {
561
+ 'content-type': 'application/json',
562
+ 'User-Agent': typeof fakeUa === 'function' ? fakeUa() : 'blockbook-client/1.0'
563
+ },
564
+ params: {
565
+ details: filter,
566
+ pageSize: 1000
567
+ }
568
+ })];
307
569
  case 2:
308
570
  resp = _a.sent();
309
- //TODO paginate?
310
571
  return [2 /*return*/, resp.data];
311
572
  case 3:
312
573
  e_5 = _a.sent();
313
- console.error(tag, e_5);
574
+ log.error(tag, "Failed to get info for address ".concat(address, ":"), e_5);
314
575
  throw e_5;
315
576
  case 4: return [2 /*return*/];
316
577
  }
317
578
  });
318
579
  });
319
- };
320
- var get_txs_by_xpub = function (coin, xpub) {
580
+ }
581
+ /**
582
+ * Get all transactions for an extended public key
583
+ * @param coin Coin symbol
584
+ * @param xpub Extended public key
585
+ * @returns Promise with xpub info including all transactions
586
+ */
587
+ function get_txs_by_xpub(coin, xpub) {
321
588
  return __awaiter(this, void 0, void 0, function () {
322
- var tag, url, body, resp, e_6;
589
+ var tag, baseUrl, url, resp, e_6;
323
590
  return __generator(this, function (_a) {
324
591
  switch (_a.label) {
325
592
  case 0:
326
- tag = TAG + " | FA get_txs_by_xpub | ";
593
+ tag = TAG + " | get_txs_by_xpub | ";
327
594
  _a.label = 1;
328
595
  case 1:
329
596
  _a.trys.push([1, 3, , 4]);
330
- url = BLOCKBOOK_URLS[coin.toUpperCase()] + "/api/v2/xpub/" + xpub + "?details=all";
331
- body = {
332
- method: 'GET',
333
- url: url,
334
- headers: {
335
- 'content-type': 'application/json',
336
- 'User-Agent': fakeUa()
337
- },
338
- };
339
- return [4 /*yield*/, axios(body)];
597
+ baseUrl = BLOCKBOOK_URLS[coin.toUpperCase()];
598
+ if (!baseUrl) {
599
+ throw new Error("Unknown coin '".concat(coin, "' or missing BLOCKBOOK_URLS entry"));
600
+ }
601
+ url = "".concat(baseUrl, "/api/v2/xpub/").concat(xpub);
602
+ log.debug(tag, "Fetching xpub transactions from:", url);
603
+ return [4 /*yield*/, axios({
604
+ method: 'GET',
605
+ url: url,
606
+ headers: {
607
+ 'content-type': 'application/json',
608
+ 'User-Agent': typeof fakeUa === 'function' ? fakeUa() : 'blockbook-client/1.0'
609
+ },
610
+ params: {
611
+ details: 'txs',
612
+ tokens: 'derived',
613
+ pageSize: 1000
614
+ }
615
+ })];
340
616
  case 2:
341
617
  resp = _a.sent();
342
618
  return [2 /*return*/, resp.data];
343
619
  case 3:
344
620
  e_6 = _a.sent();
345
- console.error(tag, e_6);
621
+ log.error(tag, "Failed to get transactions for xpub:", e_6);
346
622
  throw e_6;
347
623
  case 4: return [2 /*return*/];
348
624
  }
349
625
  });
350
626
  });
351
- };
352
- var broadcast_transaction = function (coin, hex) {
627
+ }
628
+ /**
629
+ * Broadcast a raw transaction to the network
630
+ * @param coin Coin symbol
631
+ * @param hex Raw transaction hex
632
+ * @returns Promise with broadcast result including success status and txid or error
633
+ */
634
+ function broadcast_transaction(coin, hex) {
353
635
  return __awaiter(this, void 0, void 0, function () {
354
- var tag, url, data, body, output, resp, e_7, errorMessage, statusCode, e_8;
355
- var _a;
356
- return __generator(this, function (_b) {
357
- switch (_b.label) {
636
+ var tag, baseUrl, url, output, resp, e_7, errorMessage, statusCode, e_8;
637
+ var _a, _b;
638
+ return __generator(this, function (_c) {
639
+ switch (_c.label) {
358
640
  case 0:
359
641
  tag = TAG + " | broadcast_transaction | ";
360
- _b.label = 1;
642
+ _c.label = 1;
361
643
  case 1:
362
- _b.trys.push([1, 6, , 7]);
363
- url = BLOCKBOOK_URLS[coin.toUpperCase()] + "/api/v2/sendtx/";
364
- data = hex;
365
- body = {
366
- url: url,
367
- headers: {
368
- 'content-type': 'application/json',
369
- 'User-Agent': fakeUa()
370
- },
371
- method: 'POST',
372
- json: false,
373
- data: data,
374
- };
644
+ _c.trys.push([1, 6, , 7]);
645
+ baseUrl = BLOCKBOOK_URLS[coin.toUpperCase()];
646
+ if (!baseUrl) {
647
+ throw new Error("Unknown coin '".concat(coin, "' or missing BLOCKBOOK_URLS entry"));
648
+ }
649
+ url = "".concat(baseUrl, "/api/v2/sendtx/");
650
+ log.debug(tag, "Broadcasting to:", url);
375
651
  output = {
376
652
  success: false
377
653
  };
378
- resp = void 0;
379
- _b.label = 2;
654
+ _c.label = 2;
380
655
  case 2:
381
- _b.trys.push([2, 4, , 5]);
382
- return [4 /*yield*/, axios(body)];
656
+ _c.trys.push([2, 4, , 5]);
657
+ return [4 /*yield*/, axios({
658
+ url: url,
659
+ method: 'POST',
660
+ headers: {
661
+ 'content-type': 'text/plain',
662
+ 'User-Agent': typeof fakeUa === 'function' ? fakeUa() : 'blockbook-client/1.0'
663
+ },
664
+ data: hex,
665
+ })];
383
666
  case 3:
384
- resp = _b.sent();
385
- output.resp = resp;
667
+ resp = _c.sent();
386
668
  output.success = true;
669
+ output.txid = ((_a = resp.data) === null || _a === void 0 ? void 0 : _a.result) || resp.data;
670
+ output.resp = resp.data;
671
+ log.info(tag, "Transaction broadcast successful: ".concat(output.txid));
387
672
  return [3 /*break*/, 5];
388
673
  case 4:
389
- e_7 = _b.sent();
390
- log.error(tag, "Broadcast error: ", e_7.message || e_7);
674
+ e_7 = _c.sent();
675
+ log.error(tag, "Broadcast error:", e_7.message || e_7);
391
676
  errorMessage = 'Unknown error occurred';
392
677
  statusCode = null;
393
678
  if (e_7.response) {
394
679
  statusCode = e_7.response.status;
395
- log.debug(tag, "HTTP Status: ", statusCode);
396
- log.debug(tag, "Response headers: ", e_7.response.headers);
680
+ log.debug(tag, "HTTP Status:", statusCode);
681
+ log.debug(tag, "Response headers:", e_7.response.headers);
397
682
  if (e_7.response.data) {
398
- log.debug(tag, "Response data: ", e_7.response.data);
683
+ log.debug(tag, "Response data:", e_7.response.data);
399
684
  if (e_7.response.data.error) {
400
685
  errorMessage = e_7.response.data.error;
401
- log.error(tag, "API Error: ", errorMessage);
402
686
  }
403
687
  else if (typeof e_7.response.data === 'string') {
404
688
  errorMessage = e_7.response.data;
@@ -412,12 +696,10 @@ var broadcast_transaction = function (coin, hex) {
412
696
  }
413
697
  }
414
698
  else if (e_7.request) {
415
- // Request was made but no response received
416
699
  errorMessage = 'Network error: No response received';
417
- log.debug(tag, "Request config: ", (_a = e_7.config) === null || _a === void 0 ? void 0 : _a.url);
700
+ log.debug(tag, "Request config:", (_b = e_7.config) === null || _b === void 0 ? void 0 : _b.url);
418
701
  }
419
702
  else {
420
- // Something else happened
421
703
  errorMessage = e_7.message || 'Request setup error';
422
704
  }
423
705
  output.error = errorMessage;
@@ -425,17 +707,23 @@ var broadcast_transaction = function (coin, hex) {
425
707
  return [3 /*break*/, 5];
426
708
  case 5: return [2 /*return*/, output];
427
709
  case 6:
428
- e_8 = _b.sent();
429
- console.error(tag, 'error: ', e_8);
710
+ e_8 = _c.sent();
711
+ log.error(tag, 'Unexpected error:', e_8);
430
712
  throw e_8;
431
713
  case 7: return [2 /*return*/];
432
714
  }
433
715
  });
434
716
  });
435
- };
436
- var get_transaction = function (coin, txid) {
717
+ }
718
+ /**
719
+ * Get detailed information about a specific transaction
720
+ * @param coin Coin symbol
721
+ * @param txid Transaction ID
722
+ * @returns Promise with transaction details
723
+ */
724
+ function get_transaction(coin, txid) {
437
725
  return __awaiter(this, void 0, void 0, function () {
438
- var tag, url, body, resp, e_9;
726
+ var tag, cacheKey, cached, baseUrl, url, resp, txData, e_9;
439
727
  return __generator(this, function (_a) {
440
728
  switch (_a.label) {
441
729
  case 0:
@@ -443,67 +731,112 @@ var get_transaction = function (coin, txid) {
443
731
  _a.label = 1;
444
732
  case 1:
445
733
  _a.trys.push([1, 3, , 4]);
446
- url = BLOCKBOOK_URLS[coin.toUpperCase()] + "/api/v2/tx/" + txid;
447
- console.log("url: ", url);
448
- body = {
449
- method: 'GET',
450
- url: url,
451
- headers: {
452
- 'content-type': 'application/json',
453
- 'User-Agent': fakeUa()
454
- },
455
- };
456
- return [4 /*yield*/, axios(body)];
734
+ cacheKey = "tx:".concat(coin, ":").concat(txid);
735
+ cached = getCached(cacheKey);
736
+ if (cached) {
737
+ log.debug(tag, "Returning cached transaction ".concat(txid));
738
+ return [2 /*return*/, cached];
739
+ }
740
+ baseUrl = BLOCKBOOK_URLS[coin.toUpperCase()];
741
+ if (!baseUrl) {
742
+ throw new Error("Unknown coin '".concat(coin, "' or missing BLOCKBOOK_URLS entry"));
743
+ }
744
+ url = "".concat(baseUrl, "/api/v2/tx/").concat(txid);
745
+ log.debug(tag, "Fetching transaction from:", url);
746
+ return [4 /*yield*/, axios({
747
+ method: 'GET',
748
+ url: url,
749
+ headers: {
750
+ 'content-type': 'application/json',
751
+ 'User-Agent': typeof fakeUa === 'function' ? fakeUa() : 'blockbook-client/1.0'
752
+ },
753
+ })];
457
754
  case 2:
458
755
  resp = _a.sent();
459
- return [2 /*return*/, resp.data];
756
+ txData = resp.data;
757
+ // Cache confirmed transactions
758
+ if (txData.confirmations > 0) {
759
+ setCache(cacheKey, txData);
760
+ }
761
+ return [2 /*return*/, txData];
460
762
  case 3:
461
763
  e_9 = _a.sent();
462
- console.error(tag, e_9);
764
+ log.error(tag, "Failed to get transaction ".concat(txid, ":"), e_9);
463
765
  throw e_9;
464
766
  case 4: return [2 /*return*/];
465
767
  }
466
768
  });
467
769
  });
468
- };
469
- var get_utxos_by_xpub = function (coin, xpub) {
470
- return __awaiter(this, void 0, void 0, function () {
471
- var tag, url, body, resp, e_10;
770
+ }
771
+ /**
772
+ * Get UTXOs (Unspent Transaction Outputs) for an extended public key
773
+ * @param coin Coin symbol
774
+ * @param xpub Extended public key
775
+ * @param confirmedOnly Whether to include only confirmed UTXOs (default false)
776
+ * @returns Promise with array of UTXOs
777
+ */
778
+ function get_utxos_by_xpub(coin_1, xpub_1) {
779
+ return __awaiter(this, arguments, void 0, function (coin, xpub, confirmedOnly) {
780
+ var tag, baseUrl, url, hasApiKeyInUrl, resp, utxos, e_10;
781
+ if (confirmedOnly === void 0) { confirmedOnly = false; }
472
782
  return __generator(this, function (_a) {
473
783
  switch (_a.label) {
474
784
  case 0:
475
- tag = TAG + " | FA get_utxos_by_xpub | ";
785
+ tag = TAG + " | get_utxos_by_xpub | ";
476
786
  _a.label = 1;
477
787
  case 1:
478
788
  _a.trys.push([1, 3, , 4]);
479
- log.info(tag, "get_utxos_by_xpub: ", BLOCKBOOK_URLS);
480
- url = BLOCKBOOK_URLS[coin.toUpperCase()] + "/api/v2/utxo/" + xpub + "?confirmed=false";
481
- console.log("url: ", url);
482
- body = {
483
- method: 'GET',
484
- url: url,
485
- // headers: {
486
- // 'api-key': NOW_NODES_API,
487
- // 'content-type': 'application/json',
488
- // 'User-Agent': fakeUa()
489
- // },
490
- };
491
- return [4 /*yield*/, axios(body)];
789
+ baseUrl = BLOCKBOOK_URLS[coin.toUpperCase()];
790
+ if (!baseUrl) {
791
+ throw new Error("Unknown coin '".concat(coin, "' or missing BLOCKBOOK_URLS entry"));
792
+ }
793
+ url = "".concat(baseUrl, "/api/v2/utxo/").concat(encodeURIComponent(xpub));
794
+ log.debug(tag, "Fetching UTXOs from:", url);
795
+ log.debug(tag, "Base URL:", baseUrl);
796
+ log.debug(tag, "Full xpub:", xpub);
797
+ hasApiKeyInUrl = baseUrl.includes(NOW_NODES_API || '');
798
+ return [4 /*yield*/, axios({
799
+ method: 'GET',
800
+ url: url,
801
+ headers: __assign({ 'content-type': 'application/json', 'User-Agent': typeof fakeUa === 'function' ? fakeUa() : 'blockbook-client/1.0' }, (NOW_NODES_API && !hasApiKeyInUrl && { 'api-key': NOW_NODES_API })),
802
+ params: {
803
+ confirmed: confirmedOnly
804
+ }
805
+ })];
492
806
  case 2:
493
807
  resp = _a.sent();
494
- return [2 /*return*/, resp.data];
808
+ // Check if we got HTML instead of JSON (error page)
809
+ if (typeof resp.data === 'string' && resp.data.includes('<!doctype html>')) {
810
+ log.error(tag, "Received HTML error page instead of JSON data");
811
+ log.error(tag, "Response content:", resp.data.substring(0, 500) + "...");
812
+ throw new Error("Blockbook server returned HTML error page for ".concat(coin, ". Check server status and xpub format."));
813
+ }
814
+ utxos = resp.data;
815
+ if (!Array.isArray(utxos)) {
816
+ log.error(tag, "Invalid response format - expected array but got:", typeof utxos);
817
+ log.error(tag, "Response data:", utxos);
818
+ throw new Error("Invalid UTXO response format from Blockbook server for ".concat(coin));
819
+ }
820
+ log.info(tag, "Found ".concat(utxos.length, " UTXOs for xpub"));
821
+ return [2 /*return*/, utxos];
495
822
  case 3:
496
823
  e_10 = _a.sent();
497
- console.error(tag, e_10);
824
+ log.error(tag, "Failed to get UTXOs for xpub:", e_10);
498
825
  throw e_10;
499
826
  case 4: return [2 /*return*/];
500
827
  }
501
828
  });
502
829
  });
503
- };
504
- var get_balance_by_xpub = function (coin, xpub) {
830
+ }
831
+ /**
832
+ * Get total balance for an extended public key in BTC (not satoshis)
833
+ * @param coin Coin symbol
834
+ * @param xpub Extended public key
835
+ * @returns Promise with balance in BTC (not satoshis)
836
+ */
837
+ function get_balance_by_xpub(coin, xpub) {
505
838
  return __awaiter(this, void 0, void 0, function () {
506
- var tag, output, balance, i, uxto, e_11;
839
+ var tag, utxos, balanceSatoshis, _i, utxos_1, utxo, value, balanceBTC, e_11;
507
840
  return __generator(this, function (_a) {
508
841
  switch (_a.label) {
509
842
  case 0:
@@ -511,41 +844,73 @@ var get_balance_by_xpub = function (coin, xpub) {
511
844
  _a.label = 1;
512
845
  case 1:
513
846
  _a.trys.push([1, 3, , 4]);
514
- log.debug(tag, "coin: ", coin);
515
- log.debug(tag, "xpub: ", xpub);
516
- return [4 /*yield*/, get_utxos_by_xpub(coin, xpub)];
847
+ log.debug(tag, "Getting balance for coin:", coin);
848
+ log.debug(tag, "xpub:", xpub);
849
+ // Validate xpub format before making API call
850
+ if (!xpub || typeof xpub !== 'string' || xpub.length < 50) {
851
+ throw new Error("Invalid xpub format: ".concat(xpub));
852
+ }
853
+ return [4 /*yield*/, get_utxos_by_xpub(coin, xpub, false)];
517
854
  case 2:
518
- output = _a.sent();
519
- log.debug(tag, "output: ", output);
520
- balance = 0;
521
- //tally
522
- for (i = 0; i < output.length; i++) {
523
- uxto = output[i];
524
- balance = balance + parseInt(uxto.value);
855
+ utxos = _a.sent();
856
+ log.debug(tag, "Processing ".concat(utxos.length, " UTXOs"));
857
+ // Validate UTXOs array
858
+ if (!Array.isArray(utxos)) {
859
+ log.error(tag, "UTXOs response is not an array:", typeof utxos);
860
+ throw new Error("Invalid UTXOs response format");
861
+ }
862
+ balanceSatoshis = 0;
863
+ for (_i = 0, utxos_1 = utxos; _i < utxos_1.length; _i++) {
864
+ utxo = utxos_1[_i];
865
+ if (!utxo || typeof utxo !== 'object') {
866
+ log.warn(tag, "Invalid UTXO object:", utxo);
867
+ continue;
868
+ }
869
+ value = parseInt(utxo.value, 10);
870
+ if (!isNaN(value) && value > 0) {
871
+ balanceSatoshis += value;
872
+ }
873
+ else {
874
+ log.warn(tag, "Invalid UTXO value:", utxo.value);
875
+ }
525
876
  }
526
- return [2 /*return*/, balance / 100000000];
877
+ balanceBTC = balanceSatoshis / 100000000;
878
+ log.info(tag, "Total balance: ".concat(balanceBTC, " BTC (").concat(balanceSatoshis, " satoshis)"));
879
+ return [2 /*return*/, balanceBTC];
527
880
  case 3:
528
881
  e_11 = _a.sent();
529
- console.error(tag, e_11);
882
+ log.error(tag, "Failed to get balance for xpub:", e_11);
883
+ // Re-throw with more context if it's a generic error
884
+ if (e_11 instanceof Error && e_11.message.includes('<!doctype html>')) {
885
+ throw new Error("Blockbook server error: Received HTML response instead of JSON data for ".concat(coin, ". Server may be down or xpub format invalid."));
886
+ }
530
887
  throw e_11;
531
888
  case 4: return [2 /*return*/];
532
889
  }
533
890
  });
534
891
  });
535
- };
536
- var get_node_info = function () {
892
+ }
893
+ /**
894
+ * Get information about the Blockbook nodes
895
+ * @returns Promise with node status information
896
+ */
897
+ function get_node_info() {
537
898
  return __awaiter(this, void 0, void 0, function () {
538
899
  var tag;
539
900
  return __generator(this, function (_a) {
540
901
  tag = TAG + " | get_node_info | ";
541
902
  try {
542
- return [2 /*return*/, true];
903
+ return [2 /*return*/, {
904
+ initialized: Object.keys(BLOCKBOOK_URLS).length > 0,
905
+ availableNodes: Object.keys(BLOCKBOOK_URLS),
906
+ activeWebsockets: Object.keys(BLOCKBOOK_SOCKETS),
907
+ }];
543
908
  }
544
909
  catch (e) {
545
- console.error(tag, e);
910
+ log.error(tag, "Failed to get node info:", e);
546
911
  throw e;
547
912
  }
548
913
  return [2 /*return*/];
549
914
  });
550
915
  });
551
- };
916
+ }