@hashgraphonline/standards-sdk 0.0.101 → 0.0.103

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.
Files changed (107) hide show
  1. package/dist/es/services/mirror-node.d.ts +102 -1
  2. package/dist/es/services/mirror-node.d.ts.map +1 -1
  3. package/dist/es/services/types.d.ts +53 -0
  4. package/dist/es/services/types.d.ts.map +1 -1
  5. package/dist/es/standards-sdk.es19.js.map +1 -1
  6. package/dist/es/standards-sdk.es21.js +508 -47
  7. package/dist/es/standards-sdk.es21.js.map +1 -1
  8. package/dist/es/standards-sdk.es24.js +10 -10
  9. package/dist/es/standards-sdk.es26.js +1 -1
  10. package/dist/es/standards-sdk.es28.js +1 -1
  11. package/dist/es/standards-sdk.es29.js +3 -3
  12. package/dist/es/standards-sdk.es33.js +2 -2
  13. package/dist/es/standards-sdk.es34.js +1 -1
  14. package/dist/es/standards-sdk.es35.js +2 -101
  15. package/dist/es/standards-sdk.es35.js.map +1 -1
  16. package/dist/es/standards-sdk.es36.js +195 -2
  17. package/dist/es/standards-sdk.es36.js.map +1 -1
  18. package/dist/es/standards-sdk.es37.js +437 -15
  19. package/dist/es/standards-sdk.es37.js.map +1 -1
  20. package/dist/es/standards-sdk.es38.js +20 -404
  21. package/dist/es/standards-sdk.es38.js.map +1 -1
  22. package/dist/es/standards-sdk.es39.js +139 -2
  23. package/dist/es/standards-sdk.es39.js.map +1 -1
  24. package/dist/es/standards-sdk.es40.js +18 -2282
  25. package/dist/es/standards-sdk.es40.js.map +1 -1
  26. package/dist/es/standards-sdk.es41.js +21 -156
  27. package/dist/es/standards-sdk.es41.js.map +1 -1
  28. package/dist/es/standards-sdk.es42.js +25 -80
  29. package/dist/es/standards-sdk.es42.js.map +1 -1
  30. package/dist/es/standards-sdk.es43.js +20 -22
  31. package/dist/es/standards-sdk.es43.js.map +1 -1
  32. package/dist/es/standards-sdk.es44.js +38 -142
  33. package/dist/es/standards-sdk.es44.js.map +1 -1
  34. package/dist/es/standards-sdk.es45.js +13 -86
  35. package/dist/es/standards-sdk.es45.js.map +1 -1
  36. package/dist/es/standards-sdk.es46.js +56 -190
  37. package/dist/es/standards-sdk.es46.js.map +1 -1
  38. package/dist/es/standards-sdk.es47.js +1207 -356
  39. package/dist/es/standards-sdk.es47.js.map +1 -1
  40. package/dist/es/standards-sdk.es48.js +144 -21
  41. package/dist/es/standards-sdk.es48.js.map +1 -1
  42. package/dist/es/standards-sdk.es49.js +82 -130
  43. package/dist/es/standards-sdk.es49.js.map +1 -1
  44. package/dist/es/standards-sdk.es50.js +79 -13
  45. package/dist/es/standards-sdk.es50.js.map +1 -1
  46. package/dist/es/standards-sdk.es51.js +28 -25
  47. package/dist/es/standards-sdk.es51.js.map +1 -1
  48. package/dist/es/standards-sdk.es52.js +5 -28
  49. package/dist/es/standards-sdk.es52.js.map +1 -1
  50. package/dist/es/standards-sdk.es53.js +155 -17
  51. package/dist/es/standards-sdk.es53.js.map +1 -1
  52. package/dist/es/standards-sdk.es54.js +22 -43
  53. package/dist/es/standards-sdk.es54.js.map +1 -1
  54. package/dist/es/standards-sdk.es55.js +788 -13
  55. package/dist/es/standards-sdk.es55.js.map +1 -1
  56. package/dist/es/standards-sdk.es56.js +136 -54
  57. package/dist/es/standards-sdk.es56.js.map +1 -1
  58. package/dist/es/standards-sdk.es57.js +174 -1289
  59. package/dist/es/standards-sdk.es57.js.map +1 -1
  60. package/dist/es/standards-sdk.es58.js +97 -28
  61. package/dist/es/standards-sdk.es58.js.map +1 -1
  62. package/dist/es/standards-sdk.es59.js +2 -6
  63. package/dist/es/standards-sdk.es59.js.map +1 -1
  64. package/dist/es/standards-sdk.es60.js +15 -158
  65. package/dist/es/standards-sdk.es60.js.map +1 -1
  66. package/dist/es/standards-sdk.es61.js +402 -786
  67. package/dist/es/standards-sdk.es61.js.map +1 -1
  68. package/dist/es/standards-sdk.es62.js +16 -169
  69. package/dist/es/standards-sdk.es62.js.map +1 -1
  70. package/dist/es/standards-sdk.es63.js +22 -138
  71. package/dist/es/standards-sdk.es63.js.map +1 -1
  72. package/dist/es/standards-sdk.es64.js +24 -19
  73. package/dist/es/standards-sdk.es64.js.map +1 -1
  74. package/dist/es/standards-sdk.es65.js +34 -23
  75. package/dist/es/standards-sdk.es65.js.map +1 -1
  76. package/dist/es/standards-sdk.es66.js +2282 -26
  77. package/dist/es/standards-sdk.es66.js.map +1 -1
  78. package/dist/es/standards-sdk.es67.js +163 -37
  79. package/dist/es/standards-sdk.es67.js.map +1 -1
  80. package/dist/es/standards-sdk.es69.js +7133 -1
  81. package/dist/es/standards-sdk.es69.js.map +1 -1
  82. package/dist/es/standards-sdk.es70.js +9 -7135
  83. package/dist/es/standards-sdk.es70.js.map +1 -1
  84. package/dist/es/standards-sdk.es71.js +10 -9
  85. package/dist/es/standards-sdk.es71.js.map +1 -1
  86. package/dist/es/standards-sdk.es72.js +16 -9
  87. package/dist/es/standards-sdk.es72.js.map +1 -1
  88. package/dist/es/standards-sdk.es73.js +4 -16
  89. package/dist/es/standards-sdk.es73.js.map +1 -1
  90. package/dist/es/standards-sdk.es74.js +8 -5
  91. package/dist/es/standards-sdk.es74.js.map +1 -1
  92. package/dist/es/standards-sdk.es75.js +418 -8
  93. package/dist/es/standards-sdk.es75.js.map +1 -1
  94. package/dist/es/standards-sdk.es76.js +2 -419
  95. package/dist/es/standards-sdk.es76.js.map +1 -1
  96. package/dist/es/standards-sdk.es77.js +120 -2
  97. package/dist/es/standards-sdk.es77.js.map +1 -1
  98. package/dist/es/standards-sdk.es78.js +3 -119
  99. package/dist/es/standards-sdk.es78.js.map +1 -1
  100. package/dist/es/standards-sdk.es79.js +1 -1
  101. package/dist/umd/services/mirror-node.d.ts +102 -1
  102. package/dist/umd/services/mirror-node.d.ts.map +1 -1
  103. package/dist/umd/services/types.d.ts +53 -0
  104. package/dist/umd/services/types.d.ts.map +1 -1
  105. package/dist/umd/standards-sdk.umd.js +12 -12
  106. package/dist/umd/standards-sdk.umd.js.map +1 -1
  107. package/package.json +2 -2
@@ -1,15 +1,36 @@
1
1
  import Buffer from "./standards-sdk.es25.js";
2
- import { PublicKey, Timestamp } from "@hashgraph/sdk";
2
+ import { PublicKey, Timestamp, AccountId } from "@hashgraph/sdk";
3
3
  import "./standards-sdk.es31.js";
4
+ import { Logger } from "./standards-sdk.es15.js";
4
5
  import { proto } from "@hashgraph/proto";
5
6
  import axios from "./standards-sdk.es32.js";
6
7
  class HederaMirrorNode {
7
8
  constructor(network, logger) {
9
+ this.maxRetries = 3;
10
+ this.initialDelayMs = 1e3;
11
+ this.maxDelayMs = 3e4;
12
+ this.backoffFactor = 2;
8
13
  this.network = network;
9
14
  this.baseUrl = this.getMirrorNodeUrl();
10
- this.logger = logger;
15
+ this.logger = logger || new Logger({
16
+ level: "debug",
17
+ module: "MirrorNode"
18
+ });
11
19
  this.isServerEnvironment = typeof window === "undefined";
12
20
  }
21
+ /**
22
+ * Configures the retry mechanism for API requests.
23
+ * @param config The retry configuration.
24
+ */
25
+ configureRetry(config) {
26
+ this.maxRetries = config.maxRetries ?? this.maxRetries;
27
+ this.initialDelayMs = config.initialDelayMs ?? this.initialDelayMs;
28
+ this.maxDelayMs = config.maxDelayMs ?? this.maxDelayMs;
29
+ this.backoffFactor = config.backoffFactor ?? this.backoffFactor;
30
+ this.logger.info(
31
+ `Retry configuration updated: maxRetries=${this.maxRetries}, initialDelayMs=${this.initialDelayMs}, maxDelayMs=${this.maxDelayMs}, backoffFactor=${this.backoffFactor}`
32
+ );
33
+ }
13
34
  getMirrorNodeUrl() {
14
35
  return this.network === "mainnet" ? "https://mainnet-public.mirrornode.hedera.com" : "https://testnet.mirrornode.hedera.com";
15
36
  }
@@ -46,29 +67,24 @@ class HederaMirrorNode {
46
67
  * @throws An error if the account ID is invalid or the memo cannot be retrieved.
47
68
  */
48
69
  async getAccountMemo(accountId) {
49
- const maxRetries = 3;
50
- for (let attempt = 0; attempt < maxRetries; attempt++) {
51
- try {
52
- const accountInfoUrl = `${this.baseUrl}/api/v1/accounts/${accountId}`;
53
- const response = await axios.get(accountInfoUrl);
54
- const accountInfo = response.data;
55
- if (accountInfo && accountInfo.memo) {
56
- return accountInfo.memo;
57
- }
58
- this.logger.error(`No memo found for account ${accountId}`);
59
- if (attempt < maxRetries - 1) {
60
- await new Promise((resolve) => setTimeout(resolve, 2e3));
61
- }
62
- } catch (e) {
63
- const error = e;
64
- const logMessage = `Error getting account memo (attempt ${attempt + 1}): ${error.message}`;
65
- this.logger.error(logMessage);
66
- if (attempt < maxRetries - 1) {
67
- await new Promise((resolve) => setTimeout(resolve, 2e3));
68
- }
70
+ this.logger.info(`Getting account memo for account ID: ${accountId}`);
71
+ const accountInfoUrl = `${this.baseUrl}/api/v1/accounts/${accountId}`;
72
+ try {
73
+ const accountInfo = await this._requestWithRetry(
74
+ accountInfoUrl
75
+ );
76
+ if (accountInfo?.memo) {
77
+ return accountInfo.memo;
69
78
  }
79
+ this.logger.warn(`No memo found for account ${accountId}`);
80
+ return null;
81
+ } catch (e) {
82
+ const error = e;
83
+ this.logger.error(
84
+ `Failed to get account memo for ${accountId} after retries: ${error.message}`
85
+ );
86
+ return null;
70
87
  }
71
- return null;
72
88
  }
73
89
  /**
74
90
  * Retrieves topic information for a given topic ID from the mirror node.
@@ -79,11 +95,12 @@ class HederaMirrorNode {
79
95
  async getTopicInfo(topicId) {
80
96
  try {
81
97
  const topicInfoUrl = `${this.baseUrl}/api/v1/topics/${topicId}`;
82
- const response = await axios.get(topicInfoUrl);
83
- return response.data;
98
+ this.logger.debug(`Fetching topic info from ${topicInfoUrl}`);
99
+ const data = await this._requestWithRetry(topicInfoUrl);
100
+ return data;
84
101
  } catch (e) {
85
102
  const error = e;
86
- const logMessage = `Error retrieving topic information: ${error.message}`;
103
+ const logMessage = `Error retrieving topic information for ${topicId} after retries: ${error.message}`;
87
104
  this.logger.error(logMessage);
88
105
  throw new Error(logMessage);
89
106
  }
@@ -114,10 +131,9 @@ class HederaMirrorNode {
114
131
  async getHBARPrice(date) {
115
132
  try {
116
133
  const timestamp = Timestamp.fromDate(date).toString();
117
- const request = await fetch(
118
- `https://mainnet-public.mirrornode.hedera.com/api/v1/network/exchangerate?timestamp=${timestamp}`
119
- );
120
- const response = await request.json();
134
+ const url = `https://mainnet-public.mirrornode.hedera.com/api/v1/network/exchangerate?timestamp=${timestamp}`;
135
+ this.logger.debug(`Fetching HBAR price from ${url}`);
136
+ const response = await this._fetchWithRetry(url);
121
137
  const usdPrice = Number(response?.current_rate?.cent_equivalent) / Number(response?.current_rate?.hbar_equivalent) / 100;
122
138
  return usdPrice;
123
139
  } catch (e) {
@@ -137,10 +153,12 @@ class HederaMirrorNode {
137
153
  this.logger.debug(`Fetching token info for ${tokenId}`);
138
154
  try {
139
155
  const tokenInfoUrl = `${this.baseUrl}/api/v1/tokens/${tokenId}`;
140
- const response = await axios.get(tokenInfoUrl);
141
- if (response.data) {
142
- this.logger.trace(`Token info found for ${tokenId}:`, response.data);
143
- return response.data;
156
+ const data = await this._requestWithRetry(
157
+ tokenInfoUrl
158
+ );
159
+ if (data) {
160
+ this.logger.trace(`Token info found for ${tokenId}:`, data);
161
+ return data;
144
162
  }
145
163
  this.logger.warn(`No token info found for ${tokenId}`);
146
164
  return null;
@@ -163,8 +181,9 @@ class HederaMirrorNode {
163
181
  const messages = [];
164
182
  while (nextUrl) {
165
183
  try {
166
- const response = await axios.get(nextUrl);
167
- const data = response.data;
184
+ const data = await this._requestWithRetry(
185
+ nextUrl
186
+ );
168
187
  if (data.messages && data.messages.length > 0) {
169
188
  for (const message of data.messages) {
170
189
  try {
@@ -197,7 +216,7 @@ class HederaMirrorNode {
197
216
  } catch (error) {
198
217
  const logMessage = `Invalid JSON message content: ${messageContent}`;
199
218
  this.logger.error(logMessage);
200
- return;
219
+ continue;
201
220
  }
202
221
  messageJson.sequence_number = message.sequence_number;
203
222
  messages.push({
@@ -215,7 +234,7 @@ class HederaMirrorNode {
215
234
  nextUrl = data.links?.next ? `${this.baseUrl}${data.links.next}` : "";
216
235
  } catch (e) {
217
236
  const error = e;
218
- const logMessage = `Error querying topic messages: ${error.message} on ${topicId}`;
237
+ const logMessage = `Error querying topic messages for topic ${topicId} (URL: ${nextUrl}) after retries: ${error.message}`;
219
238
  this.logger.error(logMessage);
220
239
  throw new Error(logMessage);
221
240
  }
@@ -231,16 +250,19 @@ class HederaMirrorNode {
231
250
  async requestAccount(accountId) {
232
251
  try {
233
252
  const accountInfoUrl = `${this.baseUrl}/api/v1/accounts/${accountId}`;
234
- const response = await axios.get(accountInfoUrl);
235
- if (!response.data) {
253
+ this.logger.debug(`Requesting account info from ${accountInfoUrl}`);
254
+ const data = await this._requestWithRetry(
255
+ accountInfoUrl
256
+ );
257
+ if (!data) {
236
258
  throw new Error(
237
- `Failed to make request to mirror node for account: ${accountId}`
259
+ `No data received from mirror node for account: ${accountId}`
238
260
  );
239
261
  }
240
- return response.data;
262
+ return data;
241
263
  } catch (e) {
242
264
  const error = e;
243
- const logMessage = `Failed to fetch account: ${error.message}`;
265
+ const logMessage = `Failed to fetch account ${accountId} after retries: ${error.message}`;
244
266
  this.logger.error(logMessage);
245
267
  throw new Error(logMessage);
246
268
  }
@@ -344,13 +366,18 @@ class HederaMirrorNode {
344
366
  `Getting information for scheduled transaction ${scheduleId}`
345
367
  );
346
368
  const url = `${this.baseUrl}/api/v1/schedules/${scheduleId}`;
347
- const response = await axios.get(url);
348
- if (response.data) {
349
- return response.data;
369
+ const data = await this._requestWithRetry(url);
370
+ if (data) {
371
+ return data;
350
372
  }
373
+ this.logger.warn(
374
+ `No schedule info found for ${scheduleId} after retries.`
375
+ );
351
376
  return null;
352
377
  } catch (error) {
353
- this.logger.error(`Error fetching schedule info: ${error.message}`);
378
+ this.logger.error(
379
+ `Error fetching schedule info for ${scheduleId} after retries: ${error.message}`
380
+ );
354
381
  return null;
355
382
  }
356
383
  }
@@ -380,6 +407,440 @@ class HederaMirrorNode {
380
407
  throw error;
381
408
  }
382
409
  }
410
+ /**
411
+ * Retrieves details for a given transaction ID or hash from the mirror node.
412
+ * @param transactionIdOrHash The ID or hash of the transaction.
413
+ * @returns A promise that resolves to the transaction details.
414
+ * @throws An error if the transaction ID/hash is invalid or details cannot be retrieved.
415
+ */
416
+ async getTransaction(transactionIdOrHash) {
417
+ this.logger.info(
418
+ `Getting transaction details for ID/hash: ${transactionIdOrHash}`
419
+ );
420
+ const endpoint = transactionIdOrHash.includes("-") ? `transactions/${transactionIdOrHash}` : `transactions/${transactionIdOrHash}`;
421
+ const transactionDetailsUrl = `${this.baseUrl}/api/v1/${endpoint}`;
422
+ try {
423
+ const response = await this._requestWithRetry(transactionDetailsUrl);
424
+ if (response?.transactions?.length > 0) {
425
+ this.logger.trace(
426
+ `Transaction details found for ${transactionIdOrHash}:`,
427
+ response.transactions[0]
428
+ );
429
+ return response.transactions[0];
430
+ }
431
+ this.logger.warn(
432
+ `No transaction details found for ${transactionIdOrHash} or unexpected response structure.`
433
+ );
434
+ return null;
435
+ } catch (e) {
436
+ const error = e;
437
+ this.logger.error(
438
+ `Failed to get transaction details for ${transactionIdOrHash} after retries: ${error.message}`
439
+ );
440
+ return null;
441
+ }
442
+ }
443
+ /**
444
+ * Private helper to make GET requests with retry logic using Axios.
445
+ */
446
+ async _requestWithRetry(url, axiosConfig) {
447
+ let attempt = 0;
448
+ let delay = this.initialDelayMs;
449
+ while (attempt < this.maxRetries) {
450
+ try {
451
+ const response = await axios.get(url, axiosConfig);
452
+ return response.data;
453
+ } catch (error) {
454
+ attempt++;
455
+ const isLastAttempt = attempt >= this.maxRetries;
456
+ const statusCode = error.response?.status;
457
+ if (statusCode && statusCode >= 400 && statusCode < 500 && statusCode !== 429) {
458
+ this.logger.error(
459
+ `Client error for ${url} (status ${statusCode}): ${error.message}. Not retrying.`
460
+ );
461
+ throw error;
462
+ }
463
+ if (isLastAttempt) {
464
+ this.logger.error(
465
+ `Max retries (${this.maxRetries}) reached for ${url}. Last error: ${error.message}`
466
+ );
467
+ throw error;
468
+ }
469
+ this.logger.warn(
470
+ `Attempt ${attempt}/${this.maxRetries} failed for ${url}: ${error.message}. Retrying in ${delay}ms...`
471
+ );
472
+ await new Promise((resolve) => setTimeout(resolve, delay));
473
+ delay = Math.min(delay * this.backoffFactor, this.maxDelayMs);
474
+ }
475
+ }
476
+ throw new Error(
477
+ `Failed to fetch data from ${url} after ${this.maxRetries} attempts.`
478
+ );
479
+ }
480
+ /**
481
+ * Private helper to make fetch requests with retry logic.
482
+ */
483
+ async _fetchWithRetry(url, fetchOptions) {
484
+ let attempt = 0;
485
+ let delay = this.initialDelayMs;
486
+ while (attempt < this.maxRetries) {
487
+ try {
488
+ const request = await fetch(url, fetchOptions);
489
+ if (!request.ok) {
490
+ if (request.status >= 400 && request.status < 500 && request.status !== 429) {
491
+ this.logger.error(
492
+ `Client error for ${url} (status ${request.status}): ${request.statusText}. Not retrying.`
493
+ );
494
+ throw new Error(
495
+ `Fetch failed with status ${request.status}: ${request.statusText} for URL: ${url}`
496
+ );
497
+ }
498
+ throw new Error(
499
+ `Fetch failed with status ${request.status}: ${request.statusText} for URL: ${url}`
500
+ );
501
+ }
502
+ const response = await request.json();
503
+ return response;
504
+ } catch (error) {
505
+ attempt++;
506
+ if (attempt >= this.maxRetries) {
507
+ this.logger.error(
508
+ `Max retries (${this.maxRetries}) reached for ${url}. Last error: ${error.message}`
509
+ );
510
+ throw error;
511
+ }
512
+ this.logger.warn(
513
+ `Attempt ${attempt}/${this.maxRetries} failed for ${url}: ${error.message}. Retrying in ${delay}ms...`
514
+ );
515
+ await new Promise((resolve) => setTimeout(resolve, delay));
516
+ delay = Math.min(delay * this.backoffFactor, this.maxDelayMs);
517
+ }
518
+ }
519
+ throw new Error(
520
+ `Failed to fetch data from ${url} after ${this.maxRetries} attempts.`
521
+ );
522
+ }
523
+ /**
524
+ * Retrieves the numerical balance (in HBAR) for a given account ID.
525
+ * @param accountId The ID of the account.
526
+ * @returns A promise that resolves to the HBAR balance or null if an error occurs.
527
+ */
528
+ async getAccountBalanceNumerical(accountId) {
529
+ this.logger.info(`Getting numerical balance for account ${accountId}`);
530
+ try {
531
+ const accountInfo = await this.requestAccount(accountId);
532
+ if (accountInfo && accountInfo.balance) {
533
+ const hbarBalance = accountInfo.balance.balance / 1e8;
534
+ return hbarBalance;
535
+ }
536
+ this.logger.warn(
537
+ `Could not retrieve balance for account ${accountId} from account info.`
538
+ );
539
+ return null;
540
+ } catch (error) {
541
+ this.logger.error(
542
+ `Error fetching numerical balance for account ${accountId}: ${error.message}`
543
+ );
544
+ return null;
545
+ }
546
+ }
547
+ /**
548
+ * Retrieves messages for a given topic ID with optional filters.
549
+ * @param topicId The ID of the topic.
550
+ * @param sequenceNumber Filter by sequence number (e.g., "gt:10", "lte:20").
551
+ * @param startTime Filter by consensus timestamp (e.g., "gt:1629400000.000000000").
552
+ * @param endTime Filter by consensus timestamp (e.g., "lt:1629500000.000000000").
553
+ * @param limit The maximum number of messages to return.
554
+ * @returns A promise that resolves to an array of HCSMessages or null.
555
+ */
556
+ async getTopicMessagesByFilter(topicId, options) {
557
+ this.logger.trace(
558
+ `Querying messages for topic ${topicId} with filters: ${JSON.stringify(
559
+ options
560
+ )}`
561
+ );
562
+ let nextUrl = `${this.baseUrl}/api/v1/topics/${topicId}/messages`;
563
+ const params = new URLSearchParams();
564
+ if (options?.limit) {
565
+ params.append("limit", options.limit.toString());
566
+ }
567
+ if (options?.sequenceNumber) {
568
+ params.append("sequencenumber", options.sequenceNumber);
569
+ }
570
+ if (options?.startTime) {
571
+ params.append("timestamp", `gte:${options.startTime}`);
572
+ }
573
+ if (options?.endTime) {
574
+ params.append("timestamp", `lt:${options.endTime}`);
575
+ }
576
+ if (options?.order) {
577
+ params.append("order", options.order);
578
+ }
579
+ const queryString = params.toString();
580
+ if (queryString) {
581
+ nextUrl += `?${queryString}`;
582
+ }
583
+ const messages = [];
584
+ let pagesFetched = 0;
585
+ const maxPages = 10;
586
+ try {
587
+ while (nextUrl && pagesFetched < maxPages) {
588
+ pagesFetched++;
589
+ const data = await this._requestWithRetry(
590
+ nextUrl
591
+ );
592
+ if (data.messages && data.messages.length > 0) {
593
+ for (const message of data.messages) {
594
+ try {
595
+ if (!message.message) {
596
+ continue;
597
+ }
598
+ let messageContent;
599
+ if (this.isServerEnvironment) {
600
+ messageContent = Buffer.from(
601
+ message.message,
602
+ "base64"
603
+ ).toString("utf-8");
604
+ } else {
605
+ messageContent = new TextDecoder().decode(
606
+ Uint8Array.from(atob(message.message), (c) => c.charCodeAt(0))
607
+ );
608
+ }
609
+ let messageJson = {};
610
+ try {
611
+ messageJson = JSON.parse(messageContent);
612
+ } catch (parseError) {
613
+ this.logger.debug(
614
+ `Message content is not valid JSON, using raw: ${messageContent}`
615
+ );
616
+ messageJson = { raw_content: messageContent };
617
+ }
618
+ const parsedContent = messageJson;
619
+ const hcsMsg = {
620
+ ...parsedContent,
621
+ consensus_timestamp: message.consensus_timestamp,
622
+ sequence_number: message.sequence_number,
623
+ payer_account_id: message.payer_account_id,
624
+ topic_id: message.topic_id,
625
+ running_hash: message.running_hash,
626
+ running_hash_version: message.running_hash_version,
627
+ chunk_info: message.chunk_info,
628
+ created: new Date(
629
+ Number(message.consensus_timestamp.split(".")[0]) * 1e3 + Number(message.consensus_timestamp.split(".")[1] || 0) / 1e6
630
+ ),
631
+ payer: message.payer_account_id
632
+ };
633
+ messages.push(hcsMsg);
634
+ } catch (error) {
635
+ this.logger.error(
636
+ `Error processing individual message: ${error.message}`
637
+ );
638
+ }
639
+ }
640
+ }
641
+ if (options?.limit && messages.length >= options.limit) break;
642
+ nextUrl = data.links?.next ? `${this.baseUrl}${data.links.next}` : "";
643
+ }
644
+ return messages;
645
+ } catch (e) {
646
+ const error = e;
647
+ this.logger.error(
648
+ `Error querying filtered topic messages for ${topicId}: ${error.message}`
649
+ );
650
+ return null;
651
+ }
652
+ }
653
+ /**
654
+ * Retrieves token balances for a given account ID.
655
+ * @param accountId The ID of the account.
656
+ * @param limit The maximum number of tokens to return.
657
+ * @returns A promise that resolves to an array of AccountTokenBalance or null.
658
+ */
659
+ async getAccountTokens(accountId, limit = 100) {
660
+ this.logger.info(`Getting tokens for account ${accountId}`);
661
+ let allTokens = [];
662
+ let url = `${this.baseUrl}/api/v1/accounts/${accountId}/tokens?limit=${limit}`;
663
+ try {
664
+ for (let i = 0; i < 10 && url; i++) {
665
+ const response = await this._requestWithRetry(
666
+ url
667
+ );
668
+ if (response && response.tokens) {
669
+ allTokens = allTokens.concat(response.tokens);
670
+ }
671
+ url = response.links?.next ? `${this.baseUrl}${response.links.next}` : "";
672
+ if (!url || limit && allTokens.length >= limit) {
673
+ if (limit && allTokens.length > limit) {
674
+ allTokens = allTokens.slice(0, limit);
675
+ }
676
+ break;
677
+ }
678
+ }
679
+ return allTokens;
680
+ } catch (error) {
681
+ this.logger.error(
682
+ `Error fetching tokens for account ${accountId}: ${error.message}`
683
+ );
684
+ return null;
685
+ }
686
+ }
687
+ /**
688
+ * Retrieves transaction details by consensus timestamp.
689
+ * @param timestamp The consensus timestamp of the transaction (e.g., "1629400000.000000000").
690
+ * @returns A promise that resolves to the transaction details or null.
691
+ */
692
+ async getTransactionByTimestamp(timestamp) {
693
+ this.logger.info(`Getting transaction by timestamp: ${timestamp}`);
694
+ const url = `${this.baseUrl}/api/v1/transactions?timestamp=${timestamp}&limit=1`;
695
+ try {
696
+ const response = await this._requestWithRetry(url);
697
+ if (response && response.transactions && response.transactions.length > 0) {
698
+ const specificTransactionId = response.transactions[0].transaction_id;
699
+ this.logger.debug(
700
+ `Transaction found by timestamp, fetching full details for ID: ${specificTransactionId}`
701
+ );
702
+ return this.getTransaction(specificTransactionId);
703
+ }
704
+ this.logger.warn(`No transaction found for timestamp: ${timestamp}`);
705
+ return null;
706
+ } catch (error) {
707
+ this.logger.error(
708
+ `Error fetching transaction by timestamp ${timestamp}: ${error.message}`
709
+ );
710
+ return null;
711
+ }
712
+ }
713
+ /**
714
+ * Retrieves NFTs for a given account ID, optionally filtered by token ID.
715
+ * @param accountId The ID of the account.
716
+ * @param tokenId Optional ID of the token to filter NFTs by.
717
+ * @param limit The maximum number of NFTs to return per page (API has its own max).
718
+ * @returns A promise that resolves to an array of NftDetail or null.
719
+ */
720
+ async getAccountNfts(accountId, tokenId, limit = 100) {
721
+ this.logger.info(
722
+ `Getting NFTs for account ${accountId}${tokenId ? ` for token ${tokenId}` : ""}`
723
+ );
724
+ let allNfts = [];
725
+ let url = `${this.baseUrl}/api/v1/accounts/${accountId}/nfts?limit=${limit}`;
726
+ if (tokenId) {
727
+ url += `&token.id=${tokenId}`;
728
+ }
729
+ try {
730
+ for (let i = 0; i < 10 && url; i++) {
731
+ const response = await this._requestWithRetry(url);
732
+ if (response && response.nfts) {
733
+ const nftsWithUri = response.nfts.map((nft) => {
734
+ let tokenUri = void 0;
735
+ if (nft.metadata) {
736
+ try {
737
+ if (this.isServerEnvironment) {
738
+ tokenUri = Buffer.from(nft.metadata, "base64").toString(
739
+ "utf-8"
740
+ );
741
+ } else {
742
+ tokenUri = new TextDecoder().decode(
743
+ Uint8Array.from(atob(nft.metadata), (c) => c.charCodeAt(0))
744
+ );
745
+ }
746
+ } catch (e) {
747
+ this.logger.warn(
748
+ `Failed to decode metadata for NFT ${nft.token_id} SN ${nft.serial_number}: ${e.message}`
749
+ );
750
+ }
751
+ }
752
+ return { ...nft, token_uri: tokenUri };
753
+ });
754
+ allNfts = allNfts.concat(nftsWithUri);
755
+ }
756
+ url = response.links?.next ? `${this.baseUrl}${response.links.next}` : "";
757
+ if (!url) break;
758
+ }
759
+ return allNfts;
760
+ } catch (error) {
761
+ this.logger.error(
762
+ `Error fetching NFTs for account ${accountId}: ${error.message}`
763
+ );
764
+ return null;
765
+ }
766
+ }
767
+ /**
768
+ * Validates NFT ownership by checking if a specific serial number of a token ID exists for an account.
769
+ * @param accountId The ID of the account.
770
+ * @param tokenId The ID of the NFT's token.
771
+ * @param serialNumber The serial number of the NFT.
772
+ * @returns A promise that resolves to the NftDetail if owned, or null otherwise.
773
+ */
774
+ async validateNFTOwnership(accountId, tokenId, serialNumber) {
775
+ this.logger.info(
776
+ `Validating ownership of NFT ${tokenId} SN ${serialNumber} for account ${accountId}`
777
+ );
778
+ try {
779
+ const nfts = await this.getAccountNfts(accountId, tokenId);
780
+ if (nfts) {
781
+ const foundNft = nfts.find(
782
+ (nft) => nft.token_id === tokenId && nft.serial_number === serialNumber
783
+ );
784
+ return foundNft || null;
785
+ }
786
+ return null;
787
+ } catch (error) {
788
+ this.logger.error(`Error validating NFT ownership: ${error.message}`);
789
+ return null;
790
+ }
791
+ }
792
+ /**
793
+ * Performs a read-only query against a smart contract (eth_call like).
794
+ * @param contractIdOrAddress The contract ID (e.g., "0.0.123") or EVM address (e.g., "0x...").
795
+ * @param functionSelector The function selector and encoded parameters (e.g., "0xabcdef12...").
796
+ * @param payerAccountId The account ID of the payer (not strictly payer for read-only, but often required as 'from').
797
+ * @param estimate Whether this is an estimate call. Mirror node might not support this directly in /contracts/call for true estimation.
798
+ * @param block Block parameter, e.g., "latest", "pending", or block number.
799
+ * @param value The value in tinybars to send with the call (for payable view/pure functions, usually 0).
800
+ * @returns A promise that resolves to the contract call query response or null.
801
+ */
802
+ async readSmartContractQuery(contractIdOrAddress, functionSelector, payerAccountId, options) {
803
+ this.logger.info(
804
+ `Reading smart contract ${contractIdOrAddress} with selector ${functionSelector}`
805
+ );
806
+ const url = `${this.baseUrl}/api/v1/contracts/call`;
807
+ const toAddress = contractIdOrAddress.startsWith("0x") ? contractIdOrAddress : `0x${AccountId.fromString(contractIdOrAddress).toSolidityAddress()}`;
808
+ const fromAddress = payerAccountId.startsWith("0x") ? payerAccountId : `0x${AccountId.fromString(payerAccountId).toSolidityAddress()}`;
809
+ const body = {
810
+ block: options?.block || "latest",
811
+ data: functionSelector,
812
+ estimate: options?.estimate || false,
813
+ from: fromAddress,
814
+ to: toAddress,
815
+ gas: options?.gas,
816
+ gasPrice: options?.gasPrice,
817
+ value: options?.value || 0
818
+ };
819
+ Object.keys(body).forEach((key) => {
820
+ const K = key;
821
+ if (body[K] === void 0) {
822
+ delete body[K];
823
+ }
824
+ });
825
+ try {
826
+ const response = await this._fetchWithRetry(
827
+ url,
828
+ {
829
+ method: "POST",
830
+ body: JSON.stringify(body),
831
+ headers: {
832
+ "Content-Type": "application/json"
833
+ }
834
+ }
835
+ );
836
+ return response;
837
+ } catch (error) {
838
+ this.logger.error(
839
+ `Error reading smart contract ${contractIdOrAddress}: ${error.message}`
840
+ );
841
+ return null;
842
+ }
843
+ }
383
844
  }
384
845
  export {
385
846
  HederaMirrorNode