Skip to content

Commit a609820

Browse files
committed
feat: add mirror node token zod validation
Signed-off-by: rozekmichal <michal.rozek@blockydevs.com>
1 parent fa1a3f9 commit a609820

File tree

10 files changed

+103
-14
lines changed

10 files changed

+103
-14
lines changed

src/__tests__/mocks/mocks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ export const makeMirrorMock = (
432432
...options.tokenInfo[tokenId],
433433
total_supply: '1000000',
434434
max_supply: '1000000',
435-
treasury: '0.0.1234',
435+
treasury_account_id: '0.0.1234',
436436
created_timestamp: '1234567890',
437437
deleted: false,
438438
default_freeze_status: false,
@@ -448,7 +448,7 @@ export const makeMirrorMock = (
448448
decimals: '8',
449449
total_supply: '1000000',
450450
max_supply: '1000000',
451-
treasury: '0.0.1234',
451+
treasury_account_id: '0.0.1234',
452452
created_timestamp: '1234567890',
453453
deleted: false,
454454
default_freeze_status: false,

src/core/services/mirrornode/__tests__/unit/hedera-mirrornode-service.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ import {
2121
createMockAccountListItemAPIResponse,
2222
createMockExchangeRateResponse,
2323
createMockGetAccountsAPIResponse,
24+
createMockMirrorNodeTokenByIdJson,
2425
createMockNftInfo,
2526
createMockTokenAirdropsResponse,
2627
createMockTokenBalancesResponse,
27-
createMockTokenInfo,
2828
createMockTopicInfo,
2929
createMockTopicMessage,
3030
createMockTopicMessagesAPIResponse,
@@ -827,10 +827,10 @@ describe('HederaMirrornodeServiceDefaultImpl', () => {
827827
describe('getTokenInfo', () => {
828828
it('should fetch token info with correct URL', async () => {
829829
const { service } = setupService();
830-
const mockTokenInfo = createMockTokenInfo();
830+
const mockJson = createMockMirrorNodeTokenByIdJson();
831831
(global.fetch as jest.Mock).mockResolvedValue({
832832
ok: true,
833-
json: jest.fn().mockResolvedValue(mockTokenInfo),
833+
json: jest.fn().mockResolvedValue(mockJson),
834834
});
835835

836836
const result = await service.getTokenInfo(TEST_TOKEN_ID);
@@ -840,6 +840,7 @@ describe('HederaMirrornodeServiceDefaultImpl', () => {
840840
);
841841
expect(result.token_id).toBe(TEST_TOKEN_ID);
842842
expect(result.symbol).toBe('TEST');
843+
expect(result.treasury_account_id).toBe('0.0.1234');
843844
});
844845

845846
it('should throw error on HTTP 404', async () => {

src/core/services/mirrornode/__tests__/unit/mocks.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ export const createMockTopicMessagesAPIResponse = (
100100
...overrides,
101101
});
102102

103+
/**
104+
* Domain `TokenInfo` after `getTokenInfo` (for mocks that bypass HTTP and return a resolved value).
105+
*/
103106
export const createMockTokenInfo = (
104107
overrides: Partial<TokenInfo> = {},
105108
): TokenInfo => ({
@@ -110,7 +113,30 @@ export const createMockTokenInfo = (
110113
total_supply: '1000000000',
111114
max_supply: '1000000000',
112115
type: 'NON_FUNGIBLE_UNIQUE',
113-
treasury: '0.0.1234',
116+
treasury_account_id: '0.0.1234',
117+
created_timestamp: '2024-01-01T12:00:00.000Z',
118+
deleted: false,
119+
default_freeze_status: false,
120+
default_kyc_status: false,
121+
pause_status: 'UNPAUSED',
122+
memo: '',
123+
...overrides,
124+
});
125+
126+
/**
127+
* Raw JSON body for GET /api/v1/tokens/{id} (Mirror Node). Use with `fetch` mocks.
128+
*/
129+
export const createMockMirrorNodeTokenByIdJson = (
130+
overrides: Record<string, unknown> = {},
131+
): Record<string, unknown> => ({
132+
token_id: '0.0.2000',
133+
symbol: 'TEST',
134+
name: 'Test Token',
135+
decimals: '6',
136+
total_supply: '1000000000',
137+
max_supply: '1000000000',
138+
type: 'NON_FUNGIBLE_UNIQUE',
139+
treasury_account_id: '0.0.1234',
114140
created_timestamp: '2024-01-01T12:00:00.000Z',
115141
deleted: false,
116142
default_freeze_status: false,

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,17 @@ import type {
2626
TransactionDetailsResponse,
2727
} from './types';
2828

29-
import { ConfigurationError, NetworkError, NotFoundError } from '@/core/errors';
29+
import {
30+
CliError,
31+
ConfigurationError,
32+
NetworkError,
33+
NotFoundError,
34+
} from '@/core/errors';
3035
import { KeyAlgorithm } from '@/core/shared/constants';
36+
import { parseWithSchema } from '@/core/shared/validation/parse-with-schema.zod';
3137
import { handleMirrorNodeErrorResponse } from '@/core/utils/handle-mirror-node-error-response';
3238

39+
import { TokenInfoSchema } from './schema';
3340
import { NetworkToBaseUrl } from './types';
3441

3542
export class HederaMirrornodeServiceDefaultImpl implements HederaMirrornodeService {
@@ -324,10 +331,13 @@ export class HederaMirrornodeServiceDefaultImpl implements HederaMirrornodeServi
324331
);
325332
}
326333

327-
return (await response.json()) as TokenInfo;
334+
return parseWithSchema(
335+
TokenInfoSchema,
336+
await response.json(),
337+
`Mirror Node GET /tokens/${tokenId}`,
338+
);
328339
} catch (error) {
329-
if (error instanceof NotFoundError || error instanceof NetworkError)
330-
throw error;
340+
if (error instanceof CliError) throw error;
331341
throw new NetworkError(`Failed to fetch token info for ${tokenId}`, {
332342
cause: error,
333343
recoverable: true,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { TokenInfo } from './types';
2+
3+
import { z } from 'zod';
4+
5+
const optionalMirrorKey = z
6+
.object({
7+
_type: z.string(),
8+
key: z.string(),
9+
})
10+
.optional();
11+
12+
export const TokenInfoSchema: z.ZodType<TokenInfo> = z.object({
13+
token_id: z.string(),
14+
symbol: z.string(),
15+
name: z.string(),
16+
decimals: z.string(),
17+
total_supply: z.string(),
18+
max_supply: z.string(),
19+
type: z.string(),
20+
treasury_account_id: z.string(),
21+
admin_key: optionalMirrorKey,
22+
kyc_key: optionalMirrorKey,
23+
freeze_key: optionalMirrorKey,
24+
wipe_key: optionalMirrorKey,
25+
supply_key: optionalMirrorKey,
26+
fee_schedule_key: optionalMirrorKey,
27+
pause_key: optionalMirrorKey,
28+
created_timestamp: z.string(),
29+
deleted: z.boolean(),
30+
default_freeze_status: z.boolean(),
31+
default_kyc_status: z.boolean(),
32+
pause_status: z.string(),
33+
memo: z.string(),
34+
});

src/core/services/mirrornode/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export interface TokenInfo {
6969
total_supply: string;
7070
max_supply: string;
7171
type: string;
72-
treasury: string;
72+
treasury_account_id: string;
7373
admin_key?: {
7474
_type: string;
7575
key: string;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { z } from 'zod';
2+
3+
import { ValidationError } from '@/core/errors';
4+
5+
export function parseWithSchema<T>(
6+
schema: z.ZodType<T>,
7+
data: unknown,
8+
context: string,
9+
): T {
10+
const result = schema.safeParse(data);
11+
if (!result.success) {
12+
throw new ValidationError(`Invalid response (${context})`, {
13+
cause: result.error,
14+
context: { issues: result.error.issues },
15+
});
16+
}
17+
return result.data;
18+
}

src/plugins/token/__tests__/unit/import.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe('token plugin - import command (ADR-007)', () => {
4646
decimals: '6',
4747
total_supply: '1000000',
4848
max_supply: '1000000',
49-
treasury: '0.0.100',
49+
treasury_account_id: '0.0.100',
5050
memo: 'Imported token memo',
5151
admin_key: { _type: 'ED25519', key: 'admin-key-123' },
5252
supply_key: { _type: 'ED25519', key: 'supply-key-456' },

src/plugins/token/commands/import/handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class TokenImportCommand implements Command {
5959
tokenId: validArgs.tokenId,
6060
name: tokenInfo.name,
6161
symbol: tokenInfo.symbol,
62-
treasuryId: tokenInfo.treasury,
62+
treasuryId: tokenInfo.treasury_account_id,
6363
adminPublicKey: tokenInfo.admin_key?.key,
6464
supplyPublicKey: tokenInfo.supply_key?.key,
6565
wipePublicKey: tokenInfo.wipe_key?.key,

src/plugins/token/utils/nft-build-output.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function buildOutput(
6666
totalSupply: tokenInfo.total_supply,
6767
maxSupply: tokenInfo.max_supply,
6868
supplyType,
69-
treasury: tokenInfo.treasury || undefined,
69+
treasury: tokenInfo.treasury_account_id || undefined,
7070
memo: tokenInfo.memo || undefined,
7171
createdTimestamp: formatHederaTimestamp(tokenInfo.created_timestamp),
7272
adminKey: tokenInfo.admin_key?.key || null,

0 commit comments

Comments
 (0)