Skip to content

Commit d50e249

Browse files
authored
feat(1707): mirror node getaccount response validation (#1728)
Signed-off-by: rozekmichal <michal.rozek@blockydevs.com>
1 parent 7e0bd18 commit d50e249

File tree

4 files changed

+152
-59
lines changed

4 files changed

+152
-59
lines changed

src/core/services/mirrornode/hedera-mirrornode-service.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { NetworkService } from '@/core/services/network/network-service.interface';
22
import type { HederaMirrornodeService } from './hedera-mirrornode-service.interface';
33
import type {
4-
AccountAPIResponse,
54
AccountListItemAPIResponse,
65
AccountListItemDto,
76
AccountResponse,
@@ -36,7 +35,11 @@ import { KeyAlgorithm } from '@/core/shared/constants';
3635
import { parseWithSchema } from '@/core/shared/validation/parse-with-schema.zod';
3736
import { handleMirrorNodeErrorResponse } from '@/core/utils/handle-mirror-node-error-response';
3837

39-
import { TokenInfoSchema } from './schema';
38+
import {
39+
AccountAPIResponseSchema,
40+
GetAccountsAPIResponseSchema,
41+
TokenInfoSchema,
42+
} from './schemas';
4043
import { NetworkToBaseUrl } from './types';
4144

4245
export class HederaMirrornodeServiceDefaultImpl implements HederaMirrornodeService {
@@ -82,7 +85,11 @@ export class HederaMirrornodeServiceDefaultImpl implements HederaMirrornodeServi
8285
return null;
8386
}
8487

85-
const data = (await response.json()) as AccountAPIResponse;
88+
const data = parseWithSchema(
89+
AccountAPIResponseSchema,
90+
await response.json(),
91+
`Mirror Node GET /accounts/${accountId}`,
92+
);
8693

8794
if (!data.account) {
8895
throw new NotFoundError(`Account ${accountId} not found`);
@@ -102,8 +109,7 @@ export class HederaMirrornodeServiceDefaultImpl implements HederaMirrornodeServi
102109
keyAlgorithm: this.getKeyAlgorithm(data.key._type),
103110
};
104111
} catch (error) {
105-
if (error instanceof NotFoundError || error instanceof NetworkError)
106-
throw error;
112+
if (error instanceof CliError) throw error;
107113
throw new NetworkError(`Failed to fetch account ${accountId}`, {
108114
cause: error,
109115
recoverable: true,
@@ -179,14 +185,21 @@ export class HederaMirrornodeServiceDefaultImpl implements HederaMirrornodeServi
179185
break;
180186
}
181187

182-
const data = (await response.json()) as GetAccountsAPIResponse;
188+
const pagePayload: GetAccountsAPIResponse = parseWithSchema(
189+
GetAccountsAPIResponseSchema,
190+
await response.json(),
191+
`Mirror Node GET /accounts (page ${fetchedPages})`,
192+
);
183193

184-
allAccounts.push(...(data.accounts ?? []));
194+
allAccounts.push(...(pagePayload.accounts ?? []));
185195
if (fetchedPages >= 100) {
186196
break;
187197
}
188-
url = data.links?.next ? this.getBaseUrl() + data.links.next : null;
198+
url = pagePayload.links?.next
199+
? this.getBaseUrl() + pagePayload.links.next
200+
: null;
189201
} catch (error) {
202+
if (error instanceof CliError) throw error;
190203
if (error instanceof NetworkError) throw error;
191204
throw new NetworkError(`Failed to fetch accounts`, {
192205
cause: error,
@@ -208,7 +221,7 @@ export class HederaMirrornodeServiceDefaultImpl implements HederaMirrornodeServi
208221
accountId: apiAccount.account,
209222
createdTimestamp: apiAccount.created_timestamp,
210223
};
211-
if (apiAccount.alias !== undefined) dto.alias = apiAccount.alias;
224+
if (apiAccount.alias != null) dto.alias = apiAccount.alias;
212225
if (apiAccount.deleted !== undefined) dto.deleted = apiAccount.deleted;
213226
if (apiAccount.memo !== undefined) dto.memo = apiAccount.memo;
214227
if (apiAccount.evm_address !== undefined)

src/core/services/mirrornode/schema.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type {
2+
AccountAPIBalance,
3+
AccountAPIKey,
4+
AccountAPIResponse,
5+
AccountListItemAPIResponse,
6+
AccountListItemBalance,
7+
AccountListItemTokenBalance,
8+
GetAccountsAPIResponse,
9+
TokenInfo,
10+
} from './types';
11+
12+
import { z } from 'zod';
13+
14+
const mirrorKeyObject = z.object({
15+
_type: z.string(),
16+
key: z.string(),
17+
});
18+
19+
const optionalKeyRef = z.union([mirrorKeyObject, z.null()]).optional();
20+
21+
export const TokenInfoSchema: z.ZodType<TokenInfo> = z.object({
22+
token_id: z.string(),
23+
symbol: z.string(),
24+
name: z.string(),
25+
decimals: z.string(),
26+
total_supply: z.string(),
27+
max_supply: z.string(),
28+
type: z.string(),
29+
treasury_account_id: z.string(),
30+
admin_key: optionalKeyRef,
31+
kyc_key: optionalKeyRef,
32+
freeze_key: optionalKeyRef,
33+
wipe_key: optionalKeyRef,
34+
supply_key: optionalKeyRef,
35+
fee_schedule_key: optionalKeyRef,
36+
pause_key: optionalKeyRef,
37+
created_timestamp: z.string(),
38+
deleted: z.boolean().nullable().optional(),
39+
freeze_default: z.boolean().optional(),
40+
pause_status: z.string(),
41+
memo: z.string(),
42+
});
43+
44+
const mirrorNodeKeyTypeSchema = z.enum(['ECDSA_SECP256K1', 'ED25519']);
45+
46+
export const AccountAPIBalanceSchema: z.ZodType<AccountAPIBalance> = z.object({
47+
balance: z.number(),
48+
timestamp: z.string(),
49+
});
50+
51+
export const AccountAPIKeySchema: z.ZodType<AccountAPIKey> = z.object({
52+
_type: mirrorNodeKeyTypeSchema,
53+
key: z.string(),
54+
});
55+
56+
export const AccountAPIResponseSchema: z.ZodType<AccountAPIResponse> = z.object(
57+
{
58+
account: z.string(),
59+
alias: z.string().nullable().optional(),
60+
balance: AccountAPIBalanceSchema,
61+
created_timestamp: z.string(),
62+
evm_address: z.string().optional(),
63+
key: AccountAPIKeySchema.optional(),
64+
max_automatic_token_associations: z.number(),
65+
memo: z.string(),
66+
receiver_sig_required: z.boolean(),
67+
},
68+
);
69+
70+
const accountListItemTokenBalanceSchema: z.ZodType<AccountListItemTokenBalance> =
71+
z.object({
72+
token_id: z.string(),
73+
balance: z.number(),
74+
});
75+
76+
export const AccountListItemBalanceSchema: z.ZodType<AccountListItemBalance> =
77+
z.object({
78+
timestamp: z.string(),
79+
balance: z.number(),
80+
tokens: z.array(accountListItemTokenBalanceSchema).optional(),
81+
});
82+
83+
export const AccountListItemAPIResponseSchema: z.ZodType<AccountListItemAPIResponse> =
84+
z.object({
85+
account: z.string(),
86+
alias: z.string().nullable().optional(),
87+
balance: AccountListItemBalanceSchema.optional(),
88+
created_timestamp: z.string(),
89+
evm_address: z.string().optional(),
90+
key: z.union([AccountAPIKeySchema, z.null()]).optional(),
91+
deleted: z.boolean().optional(),
92+
memo: z.string().optional(),
93+
});
94+
95+
export const GetAccountsAPIResponseSchema: z.ZodType<GetAccountsAPIResponse> =
96+
z.object({
97+
accounts: z.array(AccountListItemAPIResponseSchema),
98+
links: z
99+
.object({
100+
next: z.string().nullable().optional(),
101+
})
102+
.optional(),
103+
});

src/core/services/mirrornode/types.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,23 @@ export const NetworkToBaseUrl = new Map<SupportedNetwork, string>([
1616

1717
export type MirrorNodeKeyType = 'ECDSA_SECP256K1' | 'ED25519';
1818

19-
// Account API Response
19+
export interface AccountAPIBalance {
20+
balance: number;
21+
timestamp: string;
22+
}
23+
24+
export interface AccountAPIKey {
25+
_type: MirrorNodeKeyType;
26+
key: string;
27+
}
28+
2029
export interface AccountAPIResponse {
2130
account: string;
22-
alias?: string;
23-
balance: {
24-
balance: number;
25-
timestamp: string;
26-
};
31+
alias?: string | null;
32+
balance: AccountAPIBalance;
2733
created_timestamp: string;
2834
evm_address?: string;
29-
key?: {
30-
_type: MirrorNodeKeyType;
31-
key: string;
32-
};
35+
key?: AccountAPIKey;
3336
max_automatic_token_associations: number;
3437
memo: string;
3538
receiver_sig_required: boolean;
@@ -281,17 +284,24 @@ export interface GetAccountsQueryParams {
281284
order?: MirrorNodeRequestOrderParameter;
282285
}
283286

287+
export interface AccountListItemTokenBalance {
288+
token_id: string;
289+
balance: number;
290+
}
291+
292+
export interface AccountListItemBalance {
293+
timestamp: string;
294+
balance: number;
295+
tokens?: AccountListItemTokenBalance[];
296+
}
297+
284298
export interface AccountListItemAPIResponse {
285299
account: string;
286-
alias?: string;
287-
balance?: {
288-
timestamp: string;
289-
balance: number;
290-
tokens?: Array<{ token_id: string; balance: number }>;
291-
};
300+
alias?: string | null;
301+
balance?: AccountListItemBalance;
292302
created_timestamp: string;
293303
evm_address?: string;
294-
key?: { _type: MirrorNodeKeyType; key: string } | null;
304+
key?: AccountAPIKey | null;
295305
deleted?: boolean;
296306
memo?: string;
297307
}

0 commit comments

Comments
 (0)