Skip to content

Commit 09a28a1

Browse files
authored
feat: harden mirror node polling (hashgraph#2509)
fix: harden mirror node polling Signed-off-by: yasenltd <yasenltd@gmail.com>
1 parent d17566a commit 09a28a1

1 file changed

Lines changed: 135 additions & 65 deletions

File tree

automation/utils/mirrorNodeAPI.ts

Lines changed: 135 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,74 @@ import retry from 'async-retry';
44
import { formatTransactionId, getNetworkEnv } from './util.js';
55
import { AccountInfo, AccountsResponse } from '../../front-end/src/shared/interfaces/index.js';
66

7-
const getBaseURL = () => {
8-
const network = getNetworkEnv().toUpperCase();
9-
switch (network) {
10-
case 'TESTNET':
11-
return 'https://testnet.mirrornode.hedera.com/api/v1';
12-
case 'PREVIEWNET':
13-
return 'https://previewnet.mirrornode.hedera.com/api/v1';
14-
case 'LOCALNET':
15-
default:
16-
return 'http://localhost:8081/api/v1';
17-
}
18-
};
19-
20-
const apiCall = async (endpoint: string, params: Object) => {
21-
const baseURL = getBaseURL();
22-
const fullURL = `${baseURL}/${endpoint}`;
23-
console.log(`Executing API Call: ${fullURL} with params:`, params);
24-
try {
25-
const response = await axios.get(fullURL, { params });
26-
console.log(`API Call successful: ${fullURL}`);
27-
return response.data;
28-
} catch (error: unknown) {
29-
throw new Error(
30-
error instanceof Error ? `API call failed: ${error.message}` : 'API call failed',
31-
);
32-
}
33-
};
7+
const getBaseURL = () => {
8+
const network = getNetworkEnv().toUpperCase();
9+
switch (network) {
10+
case 'TESTNET':
11+
return 'https://testnet.mirrornode.hedera.com/api/v1';
12+
case 'PREVIEWNET':
13+
return 'https://previewnet.mirrornode.hedera.com/api/v1';
14+
case 'LOCALNET':
15+
default:
16+
return 'http://localhost:8081/api/v1';
17+
}
18+
};
19+
20+
const apiCall = async (endpoint: string, params: Object) => {
21+
const baseURL = getBaseURL();
22+
const fullURL = `${baseURL}/${endpoint}`;
23+
console.log(`Executing API Call: ${fullURL} with params:`, params);
24+
try {
25+
const response = await axios.get(fullURL, { params });
26+
console.log(`API Call successful: ${fullURL}`);
27+
return response.data;
28+
} catch (error: unknown) {
29+
throw new Error(
30+
error instanceof Error ? `API call failed: ${error.message}` : 'API call failed',
31+
);
32+
}
33+
};
34+
35+
const summarizeTransactions = (response: any) => {
36+
return (response?.transactions ?? []).map((transaction: any) => ({
37+
transaction_id: transaction.transaction_id,
38+
consensus_timestamp: transaction.consensus_timestamp,
39+
name: transaction.name,
40+
result: transaction.result,
41+
}));
42+
};
43+
44+
const logRecentTransactionsForDebug = async (payerAccountId: string) => {
45+
try {
46+
const allTransactions = await apiCall('transactions', { limit: 10, order: 'desc' });
47+
console.log(
48+
'[mirror-node-debug] Recent transactions from /transactions:',
49+
summarizeTransactions(allTransactions),
50+
);
51+
} catch (listError) {
52+
console.log(
53+
'[mirror-node-debug] Failed to fetch recent transactions from /transactions:',
54+
listError instanceof Error ? listError.message : listError,
55+
);
56+
}
57+
58+
try {
59+
const payerTransactions = await apiCall('transactions', {
60+
'account.id': payerAccountId,
61+
limit: 10,
62+
order: 'desc',
63+
});
64+
console.log(
65+
`[mirror-node-debug] Recent transactions from /transactions for payer ${payerAccountId}:`,
66+
summarizeTransactions(payerTransactions),
67+
);
68+
} catch (listError) {
69+
console.log(
70+
`[mirror-node-debug] Failed to fetch payer transactions from /transactions for ${payerAccountId}:`,
71+
listError instanceof Error ? listError.message : listError,
72+
);
73+
}
74+
};
3475

3576
/**
3677
* Performs a polling with retry mechanism on the mirror node API endpoint until a condition is met.
@@ -44,8 +85,8 @@ import { AccountInfo, AccountsResponse } from '../../front-end/src/shared/interf
4485
* @param {Object} params - The parameters to pass with the API call, usually query parameters.
4586
* @param {Function} validateResult - A function to validate the result of the API call.
4687
* Should return `true` if the result meets the expected conditions, `false` otherwise.
47-
* @param {number} [timeout=15000] - The maximum time in milliseconds to keep retrying the API call.
48-
* @param {number} [interval=2500] - The interval in milliseconds between retries.
88+
* @param {number} [timeout=30000] - The maximum time in milliseconds to keep retrying the API call.
89+
* @param {number} [interval=2000] - The interval in milliseconds between retries.
4990
* @returns {Promise<Object>} - A promise that resolves with the data from the API once the validation condition is met.
5091
* If the timeout is reached without successful validation, the promise rejects.
5192
*
@@ -56,52 +97,79 @@ import { AccountInfo, AccountsResponse } from '../../front-end/src/shared/interf
5697
* .catch(error => console.error('Failed to fetch account details:', error));
5798
* ```
5899
*/
59-
const pollWithRetry = async (
60-
endpoint: string,
61-
params: Object,
62-
validateResult: (result: any) => boolean,
63-
timeout: number = 20000,
64-
interval: number = 2500,
65-
): Promise<any> => {
66-
return retry(
67-
async () => {
68-
console.log(`Fetching data from ${endpoint}`);
69-
const result = await apiCall(endpoint, params);
70-
if (validateResult(result)) {
71-
console.log(`Validation successful for data from ${endpoint}`);
72-
return result;
73-
}
74-
throw new Error('Data not ready or condition not met');
75-
},
76-
{
77-
retries: Math.floor(timeout / interval),
78-
minTimeout: interval,
79-
maxTimeout: interval,
80-
onRetry: (error: any) => {
81-
console.log(`Retrying due to: ${error.message}`);
82-
},
83-
},
84-
);
85-
};
86-
87-
export const getAccountDetails = async (accountId: string) => {
100+
const pollWithRetry = async (
101+
endpoint: string,
102+
params: Object,
103+
validateResult: (result: any) => boolean,
104+
timeout: number = 30000,
105+
interval: number = 2000,
106+
): Promise<any> => {
107+
return retry(
108+
async () => {
109+
console.log(`Fetching data from ${endpoint}`);
110+
const result = await apiCall(endpoint, params);
111+
if (validateResult(result)) {
112+
console.log(`Validation successful for data from ${endpoint}`);
113+
return result;
114+
}
115+
throw new Error('Data not ready or condition not met');
116+
},
117+
{
118+
retries: Math.floor(timeout / interval),
119+
minTimeout: interval,
120+
maxTimeout: interval,
121+
onRetry: (error: any) => {
122+
console.log(`Retrying due to: ${error.message}`);
123+
},
124+
},
125+
);
126+
};
127+
128+
export const getAccountDetails = async (
129+
accountId: string,
130+
timeout: number = 180000,
131+
interval: number = 3000,
132+
) => {
88133
return pollWithRetry(
89134
'accounts',
90135
{ 'account.id': accountId },
91136
result => result && result.accounts && result.accounts.length > 0,
137+
timeout,
138+
interval,
92139
);
93140
};
94141

95-
export const getTransactionDetails = async (transactionId: string) => {
142+
export const getTransactionDetails = async (
143+
transactionId: string,
144+
timeout: number = 180000,
145+
interval: number = 3000,
146+
) => {
96147
const formatedTransactionId = formatTransactionId(transactionId);
97-
return pollWithRetry(
98-
`transactions/${formatedTransactionId}`,
99-
{},
100-
result => result && result.transactions && result.transactions.length > 0,
101-
);
148+
const payerAccountId = transactionId.split('@')[0];
149+
150+
try {
151+
return await pollWithRetry(
152+
`transactions/${formatedTransactionId}`,
153+
{},
154+
result => result && result.transactions && result.transactions.length > 0,
155+
timeout,
156+
interval,
157+
);
158+
} catch (error) {
159+
console.log(
160+
`[mirror-node-debug] Exact transaction lookup failed for ${formatedTransactionId}. Fetching transaction lists for comparison.`,
161+
);
162+
await logRecentTransactionsForDebug(payerAccountId);
163+
164+
throw error;
165+
}
102166
};
103167

104-
export const getAssociatedAccounts = async (publicKey: string) => {
168+
export const getAssociatedAccounts = async (
169+
publicKey: string,
170+
timeout: number = 90000,
171+
interval: number = 3000,
172+
) => {
105173
let allAccounts: string[] = [];
106174
let params: Object | null = { 'account.publickey': publicKey, order: 'asc' };
107175
let endpoint = 'accounts';
@@ -112,6 +180,8 @@ export const getAssociatedAccounts = async (publicKey: string) => {
112180
endpoint,
113181
params,
114182
result => result && result.accounts && result.accounts.length > 0,
183+
timeout,
184+
interval,
115185
);
116186

117187
// Extract the account IDs from the response

0 commit comments

Comments
 (0)