diff --git a/packages/metadata/src/exceptions/fileNotFound.exception.ts b/packages/metadata/src/exceptions/fileNotFound.exception.ts new file mode 100644 index 0000000..5ab487d --- /dev/null +++ b/packages/metadata/src/exceptions/fileNotFound.exception.ts @@ -0,0 +1,5 @@ +export class FileNotFound extends Error { + constructor(path: string) { + super(`File not found at path: ${path}`); + } +} diff --git a/packages/metadata/src/exceptions/index.ts b/packages/metadata/src/exceptions/index.ts index b630137..3f40ae4 100644 --- a/packages/metadata/src/exceptions/index.ts +++ b/packages/metadata/src/exceptions/index.ts @@ -1,2 +1,3 @@ export * from "./invalidSchema.exception.js"; export * from "./fetchError.exception.js"; +export * from "./fileNotFound.exception.js"; diff --git a/packages/metadata/src/external.ts b/packages/metadata/src/external.ts index 5d2f0ff..2955e52 100644 --- a/packages/metadata/src/external.ts +++ b/packages/metadata/src/external.ts @@ -1,5 +1,9 @@ export type { IMetadataProvider } from "./internal.js"; -export { InvalidSchema, FetchError } from "./internal.js"; +export { InvalidSchema, FetchError, FileNotFound } from "./internal.js"; -export { StaticMetadataProvider, GithubMetadataProvider } from "./internal.js"; +export { + StaticMetadataProvider, + GithubMetadataProvider, + LocalFileMetadataProvider, +} from "./internal.js"; diff --git a/packages/metadata/src/interfaces/metadata.interface.ts b/packages/metadata/src/interfaces/metadata.interface.ts index 1313b14..929aaed 100644 --- a/packages/metadata/src/interfaces/metadata.interface.ts +++ b/packages/metadata/src/interfaces/metadata.interface.ts @@ -1,6 +1,30 @@ import { Token, TokenType, ZKChainMetadata } from "@zkchainhub/shared"; +/** + * Represents a metadata provider that retrieves chains and tokens metadata. + */ export interface IMetadataProvider { + /** + * Retrieves the metadata for ZK chains of the ecosystem + * + * @returns A promise that resolves to the metadata of ZK chains. + * + * @throws {FetchError} + * If there is an issue with the network request. + * + * + * @throws {InvalidSchema} + * If the response data is invalid or cannot be parsed. + */ getChainsMetadata(): Promise; + + /** + * Retrieves metadata for tokens of the ecosystem + * + * @returns A promise that resolves to an array of token metadata. + * + * @throws {FetchError} If there is an issue with the network request. + * @throws {InvalidSchema} If the response data is invalid or cannot be parsed. + */ getTokensMetadata(): Promise[]>; } diff --git a/packages/metadata/src/providers/githubMetadata.provider.ts b/packages/metadata/src/providers/githubMetadata.provider.ts index 4c533e5..b38fa1b 100644 --- a/packages/metadata/src/providers/githubMetadata.provider.ts +++ b/packages/metadata/src/providers/githubMetadata.provider.ts @@ -2,6 +2,7 @@ import axios, { AxiosInstance } from "axios"; import { z } from "zod"; import { + Cache, ILogger, Token, TokenType, @@ -13,6 +14,8 @@ import { IMetadataProvider } from "../interfaces/index.js"; import { FetchError, InvalidSchema } from "../internal.js"; import { ChainSchema, TokenSchema } from "../schemas/index.js"; +export const GITHUB_METADATA_PREFIX = "github-metadata"; + /** * Represents a provider for retrieving metadata from GitHub. */ @@ -22,6 +25,7 @@ export class GithubMetadataProvider implements IMetadataProvider { private readonly tokenJsonUrl: string, private readonly chainJsonUrl: string, private readonly logger: ILogger, + private readonly cache: Cache, ) { this.axios = axios.create({ headers: { @@ -30,44 +34,64 @@ export class GithubMetadataProvider implements IMetadataProvider { }); } + /** @inheritdoc */ async getChainsMetadata(): Promise { - const { data } = await this.axios.get(this.chainJsonUrl).catch((e) => { - this.logger.error( - `Failed to fetch chains metadata from ${this.chainJsonUrl}: ${e.message}`, - ); - throw new FetchError(`Failed to fetch chains metadata: ${e.message}`); - }); + let cachedData = await this.cache.get( + `${GITHUB_METADATA_PREFIX}-chains`, + ); + if (!cachedData) { + const { data } = await this.axios.get(this.chainJsonUrl).catch((e) => { + this.logger.error( + `Failed to fetch chains metadata from ${this.chainJsonUrl}: ${e.message}`, + ); + throw new FetchError(`Failed to fetch chains metadata: ${e.message}`); + }); + + const validatedData = z.array(ChainSchema).safeParse(data); - const validatedData = z.array(ChainSchema).safeParse(data); + if (!validatedData.success) { + this.logger.error(`Invalid ZKChain metadata: ${validatedData.error.errors}`); + throw new InvalidSchema("Invalid ZKChain metadata"); + } - if (!validatedData.success) { - this.logger.error(`Invalid ZKChain metadata: ${validatedData.error.errors}`); - throw new InvalidSchema("Invalid ZKChain metadata"); + cachedData = validatedData.data.reduce((acc, chain) => { + const { chainId, ...rest } = chain; + const chainIdBn = BigInt(chainId); + acc.set(chainIdBn, { ...rest, chainId: chainIdBn }); + return acc; + }, new Map()); + + await this.cache.set(`${GITHUB_METADATA_PREFIX}-chains`, cachedData); } - return validatedData.data.reduce((acc, chain) => { - const { chainId, ...rest } = chain; - const chainIdBn = BigInt(chainId); - acc.set(chainIdBn, { ...rest, chainId: chainIdBn }); - return acc; - }, new Map()); + return cachedData; } + /** @inheritdoc */ async getTokensMetadata(): Promise[]> { - const { data } = await this.axios.get(this.tokenJsonUrl).catch((e) => { - this.logger.error( - `Failed to fetch chains metadata from ${this.chainJsonUrl}: ${e.message}`, - ); - throw new FetchError(`Failed to fetch chains metadata: ${e.message}`); - }); + let cachedData = await this.cache.get[] | undefined>( + `${GITHUB_METADATA_PREFIX}-tokens`, + ); - const validatedData = z.array(TokenSchema).safeParse(data); + if (!cachedData) { + const { data } = await this.axios.get(this.tokenJsonUrl).catch((e) => { + this.logger.error( + `Failed to fetch chains metadata from ${this.chainJsonUrl}: ${e.message}`, + ); + throw new FetchError(`Failed to fetch chains metadata: ${e.message}`); + }); - if (!validatedData.success) { - this.logger.error(`Invalid Token metadata: ${validatedData.error.errors}`); - throw new InvalidSchema("Invalid Token metadata"); - } + const validatedData = z.array(TokenSchema).safeParse(data); + + if (!validatedData.success) { + this.logger.error(`Invalid Token metadata: ${validatedData.error.errors}`); + throw new InvalidSchema("Invalid Token metadata"); + } - return validatedData.data; + cachedData = validatedData.data; + + await this.cache.set(`${GITHUB_METADATA_PREFIX}-tokens`, cachedData); + } + return cachedData; } } diff --git a/packages/metadata/src/providers/index.ts b/packages/metadata/src/providers/index.ts index 7011ce4..74b64bc 100644 --- a/packages/metadata/src/providers/index.ts +++ b/packages/metadata/src/providers/index.ts @@ -1,2 +1,3 @@ export * from "./githubMetadata.provider.js"; export * from "./staticMetadata.provider.js"; +export * from "./localFileMetadata.provider.js"; diff --git a/packages/metadata/src/providers/localFileMetadata.provider.ts b/packages/metadata/src/providers/localFileMetadata.provider.ts new file mode 100644 index 0000000..a53e4ec --- /dev/null +++ b/packages/metadata/src/providers/localFileMetadata.provider.ts @@ -0,0 +1,91 @@ +import { existsSync, readFileSync } from "fs"; +import { z } from "zod"; + +import { + ILogger, + Token, + TokenType, + ZKChainMetadata, + ZKChainMetadataItem, +} from "@zkchainhub/shared"; + +import { FileNotFound, IMetadataProvider, InvalidSchema } from "../internal.js"; +import { ChainSchema, TokenSchema } from "../schemas/index.js"; + +export const LOCALFILE_METADATA_PREFIX = "local-metadata"; + +/** + * Represents a provider that retrieves metadata from local files. + * Note: Files are read only once and saved to instance variables. + */ +export class LocalFileMetadataProvider implements IMetadataProvider { + private readonly chainMetadata: ZKChainMetadata; + private readonly tokenMetadata: Token[]; + + /** + * Constructs a new instance of the LocalFileMetadataProvider class. + * @param tokenJsonPath The path to the token JSON file. + * @param chainJsonPath The path to the chain JSON file. + * @param logger The logger instance. + * @throws {FileNotFound} if any of the files is not found. + */ + constructor( + private readonly tokenJsonPath: string, + private readonly chainJsonPath: string, + private readonly logger: ILogger, + ) { + if (!existsSync(tokenJsonPath)) { + throw new FileNotFound(tokenJsonPath); + } + + if (!existsSync(chainJsonPath)) { + throw new FileNotFound(chainJsonPath); + } + + this.tokenMetadata = this.readAndParseTokenMetadata(); + this.chainMetadata = this.readAndParseChainMetadata(); + } + + /** @inheritdoc */ + async getChainsMetadata(): Promise { + return Promise.resolve(this.chainMetadata); + } + + readAndParseChainMetadata() { + const jsonData = readFileSync(this.chainJsonPath, "utf-8"); + const parsed = JSON.parse(jsonData); + + const validatedData = z.array(ChainSchema).safeParse(parsed); + + if (!validatedData.success) { + this.logger.error(`Invalid ZKChains metadata: ${validatedData.error.errors}`); + throw new InvalidSchema("Invalid ZKChains metadata"); + } + + return validatedData.data.reduce((acc, chain) => { + const { chainId, ...rest } = chain; + const chainIdBn = BigInt(chainId); + acc.set(chainIdBn, { ...rest, chainId: chainIdBn }); + return acc; + }, new Map()); + } + + /** @inheritdoc */ + async getTokensMetadata(): Promise[]> { + return Promise.resolve(this.tokenMetadata); + } + + readAndParseTokenMetadata() { + const jsonData = readFileSync(this.tokenJsonPath, "utf-8"); + const parsed = JSON.parse(jsonData); + + const validatedData = z.array(TokenSchema).safeParse(parsed); + + if (!validatedData.success) { + this.logger.error(`Invalid Tokens metadata: ${validatedData.error.errors}`); + throw new InvalidSchema("Invalid Tokens metadata"); + } + + return validatedData.data; + } +} diff --git a/packages/metadata/src/providers/staticMetadata.provider.ts b/packages/metadata/src/providers/staticMetadata.provider.ts index 39b08c4..fda0440 100644 --- a/packages/metadata/src/providers/staticMetadata.provider.ts +++ b/packages/metadata/src/providers/staticMetadata.provider.ts @@ -2,10 +2,16 @@ import { Token, tokens, TokenType, ZKChainMetadata, zkChainsMetadata } from "@zk import { IMetadataProvider } from "../interfaces/index.js"; +/** + * Represents a provider that retrieves metadata from static data of mainnet. + */ export class StaticMetadataProvider implements IMetadataProvider { + /** @inheritdoc */ async getChainsMetadata(): Promise { return structuredClone(zkChainsMetadata); } + + /** @inheritdoc */ async getTokensMetadata(): Promise[]> { return Array.from(tokens); } diff --git a/packages/metadata/test/fixtures/metadata.fixtures.ts b/packages/metadata/test/fixtures/metadata.fixtures.ts index 2e5422e..779f2d3 100644 --- a/packages/metadata/test/fixtures/metadata.fixtures.ts +++ b/packages/metadata/test/fixtures/metadata.fixtures.ts @@ -1,6 +1,8 @@ +import { Token, TokenType } from "@zkchainhub/shared"; + export const tokenJsonUrl = "https://example.com/tokens.json"; export const chainJsonUrl = "https://example.com/chains.json"; -export const mockTokenData = [ +export const mockTokenData: Token[] = [ { name: "Ethereum", symbol: "ETH", @@ -20,7 +22,7 @@ export const mockTokenData = [ type: "erc20", decimals: 18, }, -]; +] as const; export const mockChainData = [ { chainId: 324, diff --git a/packages/metadata/test/unit/providers/githubMetadata.provider.spec.ts b/packages/metadata/test/unit/providers/githubMetadata.provider.spec.ts index 725b07b..f40860b 100644 --- a/packages/metadata/test/unit/providers/githubMetadata.provider.spec.ts +++ b/packages/metadata/test/unit/providers/githubMetadata.provider.spec.ts @@ -1,7 +1,7 @@ import MockAdapter from "axios-mock-adapter"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { ILogger, ZKChainMetadataItem } from "@zkchainhub/shared"; +import { Cache, ILogger, ZKChainMetadataItem } from "@zkchainhub/shared"; import { FetchError, InvalidSchema } from "../../../src/internal"; import { GithubMetadataProvider } from "../../../src/providers/githubMetadata.provider"; @@ -19,15 +19,102 @@ const mockLogger: ILogger = { debug: vi.fn(), }; +const mockCache: Cache = { + store: {} as any, + get: vi.fn(), + set: vi.fn(), + del: vi.fn(), + reset: vi.fn(), +}; + describe("GithubMetadataProvider", () => { beforeEach(() => { vi.resetAllMocks(); }); describe("getChainsMetadata", () => { - it("should return the chains metadata", async () => { - const provider = new GithubMetadataProvider(tokenJsonUrl, chainJsonUrl, mockLogger); + it("return the cached chains metadata", async () => { + const cachedData = new Map([ + [ + 324n, + { + chainId: 324n, + name: "ZKsyncERA", + iconUrl: "https://s2.coinmarketcap.com/static/img/coins/64x64/24091.png", + publicRpcs: [ + "https://mainnet.era.zksync.io", + "https://zksync.drpc.org", + "https://zksync.meowrpc.com", + ], + explorerUrl: "https://explorer.zksync.io/", + launchDate: 1679626800, + chainType: "Rollup", + baseToken: { + name: "Ethereum", + symbol: "ETH", + coingeckoId: "ethereum", + type: "native", + contractAddress: null, + decimals: 18, + imageUrl: + "https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628", + }, + }, + ], + ]); + vi.spyOn(mockCache, "get").mockResolvedValue(cachedData); + const provider = new GithubMetadataProvider( + tokenJsonUrl, + chainJsonUrl, + mockLogger, + mockCache, + ); + + const axiosSpy = vi.spyOn(provider["axios"], "get"); + const result = await provider.getChainsMetadata(); + + expect(axiosSpy).not.toHaveBeenCalled(); + expect(mockCache.get).toHaveBeenCalledWith("github-metadata-chains"); + expect(result).toBeInstanceOf(Map); + expect(result.size).toBe(1); + + const chain1 = result.get(324n) as ZKChainMetadataItem; + expect(chain1).toBeDefined(); + expect(chain1).toMatchObject({ + chainId: 324n, + name: "ZKsyncERA", + iconUrl: "https://s2.coinmarketcap.com/static/img/coins/64x64/24091.png", + publicRpcs: [ + "https://mainnet.era.zksync.io", + "https://zksync.drpc.org", + "https://zksync.meowrpc.com", + ], + explorerUrl: "https://explorer.zksync.io/", + launchDate: 1679626800, + chainType: "Rollup", + baseToken: { + name: "Ethereum", + symbol: "ETH", + coingeckoId: "ethereum", + type: "native", + contractAddress: null, + decimals: 18, + imageUrl: + "https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628", + }, + }); + }); + + it("fetches and return the chains metadata", async () => { + const provider = new GithubMetadataProvider( + tokenJsonUrl, + chainJsonUrl, + mockLogger, + mockCache, + ); const axios = provider["axios"]; + + vi.spyOn(mockCache, "get").mockResolvedValue(undefined); const axiosGetMock = vi .spyOn(axios, "get") .mockResolvedValueOnce({ data: mockChainData }); @@ -35,6 +122,7 @@ describe("GithubMetadataProvider", () => { const result = await provider.getChainsMetadata(); expect(axiosGetMock).toHaveBeenCalledWith(chainJsonUrl); + expect(mockCache.set).toHaveBeenCalled(); expect(result).toBeInstanceOf(Map); expect(result.size).toBe(2); @@ -86,9 +174,15 @@ describe("GithubMetadataProvider", () => { }); it("should throw an error if the schema is invalid", async () => { - const provider = new GithubMetadataProvider(tokenJsonUrl, chainJsonUrl, mockLogger); + const provider = new GithubMetadataProvider( + tokenJsonUrl, + chainJsonUrl, + mockLogger, + mockCache, + ); const axios = provider["axios"]; + vi.spyOn(mockCache, "get").mockResolvedValue(undefined); const axiosGetMock = vi .spyOn(axios, "get") .mockResolvedValueOnce({ data: [{ invalid: "data" }] }); @@ -98,9 +192,15 @@ describe("GithubMetadataProvider", () => { }); it("should throw an error if the fetch fails with 404 error", async () => { - const provider = new GithubMetadataProvider(tokenJsonUrl, chainJsonUrl, mockLogger); + const provider = new GithubMetadataProvider( + tokenJsonUrl, + chainJsonUrl, + mockLogger, + mockCache, + ); const axios = new MockAdapter(provider["axios"]); + vi.spyOn(mockCache, "get").mockResolvedValue(undefined); axios.onGet().replyOnce(404, { data: {}, status: 404, @@ -111,9 +211,15 @@ describe("GithubMetadataProvider", () => { }); it("should throw an error if the fetch fails with 500 error", async () => { - const provider = new GithubMetadataProvider(tokenJsonUrl, chainJsonUrl, mockLogger); + const provider = new GithubMetadataProvider( + tokenJsonUrl, + chainJsonUrl, + mockLogger, + mockCache, + ); const axios = new MockAdapter(provider["axios"]); + vi.spyOn(mockCache, "get").mockResolvedValue(undefined); axios.onGet().replyOnce(500, { data: {}, status: 500, @@ -125,9 +231,33 @@ describe("GithubMetadataProvider", () => { }); describe("getTokensMetadata", () => { - it("should return the tokens metadata", async () => { - const provider = new GithubMetadataProvider(tokenJsonUrl, chainJsonUrl, mockLogger); + it("returns cached tokens metadata", async () => { + const provider = new GithubMetadataProvider( + tokenJsonUrl, + chainJsonUrl, + mockLogger, + mockCache, + ); + const axiosSpy = vi.spyOn(provider["axios"], "get"); + vi.spyOn(mockCache, "get").mockResolvedValue(mockTokenData); + + const result = await provider.getTokensMetadata(); + + expect(mockCache.get).toHaveBeenCalledWith("github-metadata-tokens"); + expect(axiosSpy).not.toHaveBeenCalled(); + expect(result).toEqual(mockTokenData); + }); + + it("fetches and return the tokens metadata", async () => { + const provider = new GithubMetadataProvider( + tokenJsonUrl, + chainJsonUrl, + mockLogger, + mockCache, + ); const axios = provider["axios"]; + + vi.spyOn(mockCache, "get").mockResolvedValue(undefined); const axiosGetMock = vi .spyOn(axios, "get") .mockResolvedValueOnce({ data: mockTokenData }); @@ -135,12 +265,20 @@ describe("GithubMetadataProvider", () => { const result = await provider.getTokensMetadata(); expect(axiosGetMock).toHaveBeenCalledWith(tokenJsonUrl); + expect(mockCache.set).toHaveBeenCalledWith("github-metadata-tokens", mockTokenData); expect(result).toEqual(mockTokenData); }); it("should throw an error if the schema is invalid", async () => { - const provider = new GithubMetadataProvider(tokenJsonUrl, chainJsonUrl, mockLogger); + const provider = new GithubMetadataProvider( + tokenJsonUrl, + chainJsonUrl, + mockLogger, + mockCache, + ); const axios = provider["axios"]; + + vi.spyOn(mockCache, "get").mockResolvedValue(undefined); const axiosGetMock = vi .spyOn(axios, "get") .mockResolvedValueOnce({ data: [{ invalid: "data" }] }); @@ -150,9 +288,15 @@ describe("GithubMetadataProvider", () => { }); it("should throw an error if the fetch fails with 404 error", async () => { - const provider = new GithubMetadataProvider(tokenJsonUrl, chainJsonUrl, mockLogger); + const provider = new GithubMetadataProvider( + tokenJsonUrl, + chainJsonUrl, + mockLogger, + mockCache, + ); const axios = new MockAdapter(provider["axios"]); + vi.spyOn(mockCache, "get").mockResolvedValue(undefined); axios.onGet().replyOnce(404, { data: {}, status: 404, @@ -163,9 +307,15 @@ describe("GithubMetadataProvider", () => { }); it("should throw an error if the fetch fails with 500 error", async () => { - const provider = new GithubMetadataProvider(tokenJsonUrl, chainJsonUrl, mockLogger); + const provider = new GithubMetadataProvider( + tokenJsonUrl, + chainJsonUrl, + mockLogger, + mockCache, + ); const axios = new MockAdapter(provider["axios"]); + vi.spyOn(mockCache, "get").mockResolvedValue(undefined); axios.onGet().replyOnce(500, { data: {}, status: 500, diff --git a/packages/metadata/test/unit/providers/localFileMetadata.provider.spec.ts b/packages/metadata/test/unit/providers/localFileMetadata.provider.spec.ts new file mode 100644 index 0000000..26cc0df --- /dev/null +++ b/packages/metadata/test/unit/providers/localFileMetadata.provider.spec.ts @@ -0,0 +1,116 @@ +import * as fs from "fs"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { ILogger } from "@zkchainhub/shared"; + +import { FileNotFound, InvalidSchema, LocalFileMetadataProvider } from "../../../src/internal"; +import { mockChainData, mockTokenData } from "../../fixtures/metadata.fixtures.js"; + +// Mock the logger +const mockLogger: ILogger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), +}; + +// Mock the file system functions +vi.mock("fs", () => ({ + existsSync: vi.fn(), + readFileSync: vi.fn(), +})); + +describe("LocalFileMetadataProvider", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("constructor", () => { + it("throws FileNotFound error if token JSON file does not exist", () => { + vi.spyOn(fs, "existsSync").mockReturnValueOnce(false); + + expect( + () => new LocalFileMetadataProvider("token.json", "chain.json", mockLogger), + ).toThrow(FileNotFound); + }); + + it("throws FileNotFound error if chain JSON file does not exist", () => { + vi.spyOn(fs, "existsSync").mockReturnValueOnce(true).mockReturnValueOnce(false); + + expect( + () => new LocalFileMetadataProvider("token.json", "chain.json", mockLogger), + ).toThrow(FileNotFound); + }); + + it("throws error on token schema validation", async () => { + const invalidTokenData = [ + { + name: "Ethereum", + symbol: "ETH", + decimals: 18, + rpcUrl: "https://mainnet.infura.io/v3/your-infura-key", + explorerUrl: "https://etherscan.io", + }, + { + name: "Wrapped Ether", + decimals: 18.5, + rpcUrl: "https://mainnet.infura.io/v3/your-infura-key", + explorerUrl: "https://etherscan.io", + }, + ]; + + vi.spyOn(fs, "existsSync").mockReturnValue(true); + vi.spyOn(fs, "readFileSync").mockReturnValueOnce(JSON.stringify(invalidTokenData)); + + expect( + () => new LocalFileMetadataProvider("token.json", "chain.json", mockLogger), + ).toThrow(InvalidSchema); + }); + + it("throws error on chain schema validation", async () => { + const invalidChainData = [ + { + name: "Ethereum", + symbol: "ETH", + decimals: 18, + rpcUrl: "https://mainnet.infura.io/v3/your-infura-key", + explorerUrl: "https://etherscan.io", + }, + { + chainId: 3, + + symbol: "BNB", + decimals: 18, + rpcUrl: "https://bsc-dataseed.binance.org", + explorerUrl: "https://bscscan.com", + }, + ]; + + vi.spyOn(fs, "existsSync").mockReturnValue(true); + vi.spyOn(fs, "readFileSync") + .mockReturnValueOnce(JSON.stringify(mockTokenData)) + .mockReturnValue(JSON.stringify(invalidChainData)); + + expect( + () => new LocalFileMetadataProvider("token.json", "chain.json", mockLogger), + ).toThrow(InvalidSchema); + }); + + it("read, parse and saves to variables the file data", async () => { + vi.spyOn(fs, "existsSync").mockReturnValue(true); + vi.spyOn(fs, "readFileSync") + .mockReturnValueOnce(JSON.stringify(mockTokenData)) + .mockReturnValueOnce(JSON.stringify(mockChainData)); + + const provider = new LocalFileMetadataProvider("token.json", "chain.json", mockLogger); + const chainMetadata = await provider.getChainsMetadata(); + const tokenMetadata = await provider.getTokensMetadata(); + + expect(provider).toBeDefined(); + expect(tokenMetadata).toEqual(provider["tokenMetadata"]); + expect(chainMetadata).toEqual(provider["chainMetadata"]); + expect(fs.readFileSync).toHaveBeenCalledWith("chain.json", "utf-8"); + expect(fs.readFileSync).toHaveBeenCalledWith("token.json", "utf-8"); + }); + }); +});