diff --git a/packages/metadata/src/providers/localFileMetadata.provider.ts b/packages/metadata/src/providers/localFileMetadata.provider.ts index 771d1cf..a53e4ec 100644 --- a/packages/metadata/src/providers/localFileMetadata.provider.ts +++ b/packages/metadata/src/providers/localFileMetadata.provider.ts @@ -2,7 +2,6 @@ import { existsSync, readFileSync } from "fs"; import { z } from "zod"; import { - Cache, ILogger, Token, TokenType, @@ -17,21 +16,23 @@ 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. - * @param cache The cache instance. * @throws {FileNotFound} if any of the files is not found. */ constructor( private readonly tokenJsonPath: string, private readonly chainJsonPath: string, private readonly logger: ILogger, - private readonly cache: Cache, ) { if (!existsSync(tokenJsonPath)) { throw new FileNotFound(tokenJsonPath); @@ -40,58 +41,51 @@ export class LocalFileMetadataProvider implements IMetadataProvider { if (!existsSync(chainJsonPath)) { throw new FileNotFound(chainJsonPath); } + + this.tokenMetadata = this.readAndParseTokenMetadata(); + this.chainMetadata = this.readAndParseChainMetadata(); } /** @inheritdoc */ async getChainsMetadata(): Promise { - let cachedData = await this.cache.get( - `${LOCALFILE_METADATA_PREFIX}-chains`, - ); - if (!cachedData) { - 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"); - } - - 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(`${LOCALFILE_METADATA_PREFIX}-chains`, cachedData); + 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 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()); } /** @inheritdoc */ async getTokensMetadata(): Promise[]> { - let cachedData = await this.cache.get[]>( - `${LOCALFILE_METADATA_PREFIX}-tokens`, - ); - if (!cachedData) { - const jsonData = readFileSync(this.tokenJsonPath, "utf-8"); - const parsed = JSON.parse(jsonData); - - const validatedData = z.array(TokenSchema).safeParse(parsed); + return Promise.resolve(this.tokenMetadata); + } - if (!validatedData.success) { - this.logger.error(`Invalid Tokens metadata: ${validatedData.error.errors}`); - throw new InvalidSchema("Invalid Tokens metadata"); - } + readAndParseTokenMetadata() { + const jsonData = readFileSync(this.tokenJsonPath, "utf-8"); + const parsed = JSON.parse(jsonData); - cachedData = validatedData.data; + const validatedData = z.array(TokenSchema).safeParse(parsed); - await this.cache.set(`${LOCALFILE_METADATA_PREFIX}-tokens`, cachedData); + if (!validatedData.success) { + this.logger.error(`Invalid Tokens metadata: ${validatedData.error.errors}`); + throw new InvalidSchema("Invalid Tokens metadata"); } - return cachedData; + return validatedData.data; } } diff --git a/packages/metadata/test/unit/providers/localFileMetadata.provider.spec.ts b/packages/metadata/test/unit/providers/localFileMetadata.provider.spec.ts index 3f42466..26cc0df 100644 --- a/packages/metadata/test/unit/providers/localFileMetadata.provider.spec.ts +++ b/packages/metadata/test/unit/providers/localFileMetadata.provider.spec.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { Cache, ILogger, ZKChainMetadataItem } from "@zkchainhub/shared"; +import { ILogger } from "@zkchainhub/shared"; import { FileNotFound, InvalidSchema, LocalFileMetadataProvider } from "../../../src/internal"; import { mockChainData, mockTokenData } from "../../fixtures/metadata.fixtures.js"; @@ -14,14 +14,6 @@ const mockLogger: ILogger = { debug: vi.fn(), }; -const mockCache: Cache = { - store: {} as any, - get: vi.fn(), - set: vi.fn(), - del: vi.fn(), - reset: vi.fn(), -}; - // Mock the file system functions vi.mock("fs", () => ({ existsSync: vi.fn(), @@ -38,13 +30,7 @@ describe("LocalFileMetadataProvider", () => { vi.spyOn(fs, "existsSync").mockReturnValueOnce(false); expect( - () => - new LocalFileMetadataProvider( - "token.json", - "chain.json", - mockLogger, - mockCache, - ), + () => new LocalFileMetadataProvider("token.json", "chain.json", mockLogger), ).toThrow(FileNotFound); }); @@ -52,76 +38,36 @@ describe("LocalFileMetadataProvider", () => { vi.spyOn(fs, "existsSync").mockReturnValueOnce(true).mockReturnValueOnce(false); expect( - () => - new LocalFileMetadataProvider( - "token.json", - "chain.json", - mockLogger, - mockCache, - ), + () => new LocalFileMetadataProvider("token.json", "chain.json", mockLogger), ).toThrow(FileNotFound); }); - it("not throws any error if both token JSON file and chain JSON file exist", () => { - vi.spyOn(fs, "existsSync").mockReturnValue(true); - expect( - () => - new LocalFileMetadataProvider( - "token.json", - "chain.json", - mockLogger, - mockCache, - ), - ).not.toThrow(); - }); - }); + 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", + }, + ]; - describe("getChainsMetadata", () => { - it("return the cached chain data if it exists", async () => { - const cachedData = new Map(); vi.spyOn(fs, "existsSync").mockReturnValue(true); - vi.spyOn(mockCache, "get").mockResolvedValue(cachedData); - - const provider = new LocalFileMetadataProvider( - "token.json", - "chain.json", - mockLogger, - mockCache, - ); - - const result = await provider.getChainsMetadata(); + vi.spyOn(fs, "readFileSync").mockReturnValueOnce(JSON.stringify(invalidTokenData)); - expect(result).toEqual(cachedData); - expect(mockCache.get).toHaveBeenCalledWith("local-metadata-chains"); - expect(fs.readFileSync).not.toHaveBeenCalled(); - }); - - it("read and parse the chain JSON file if the cached chain data does not exist", async () => { - vi.spyOn(mockCache, "get").mockResolvedValue(undefined); - vi.spyOn(fs, "readFileSync").mockReturnValue(JSON.stringify(mockChainData)); - vi.spyOn(fs, "existsSync").mockReturnValue(true); - const expectedMap = new Map(); - for (const chain of mockChainData) { - const { chainId, ...rest } = chain; - const chainIdBn = BigInt(chainId); - expectedMap.set(chainIdBn, { ...rest, chainId: chainIdBn } as ZKChainMetadataItem); - } - - const provider = new LocalFileMetadataProvider( - "token.json", - "chain.json", - mockLogger, - mockCache, - ); - - const result = await provider.getChainsMetadata(); - - expect(result).toEqual(expectedMap); - expect(fs.readFileSync).toHaveBeenCalledWith("chain.json", "utf-8"); - expect(mockCache.set).toHaveBeenCalledWith("local-metadata-chains", expectedMap); + expect( + () => new LocalFileMetadataProvider("token.json", "chain.json", mockLogger), + ).toThrow(InvalidSchema); }); - it("throws an error if schema validation fails", async () => { + it("throws error on chain schema validation", async () => { const invalidChainData = [ { name: "Ethereum", @@ -140,88 +86,31 @@ describe("LocalFileMetadataProvider", () => { }, ]; - vi.spyOn(mockCache, "get").mockResolvedValue(undefined); - vi.spyOn(fs, "readFileSync").mockReturnValue(JSON.stringify(invalidChainData)); - vi.spyOn(fs, "existsSync").mockReturnValue(true); - - const provider = new LocalFileMetadataProvider( - "token.json", - "chain.json", - mockLogger, - mockCache, - ); - - await expect(provider.getChainsMetadata()).rejects.toThrow(InvalidSchema); - }); - }); - - describe("getTokensMetadata", () => { - it("returns the cached token data if it exists", async () => { vi.spyOn(fs, "existsSync").mockReturnValue(true); - vi.spyOn(mockCache, "get").mockResolvedValue(mockTokenData); + vi.spyOn(fs, "readFileSync") + .mockReturnValueOnce(JSON.stringify(mockTokenData)) + .mockReturnValue(JSON.stringify(invalidChainData)); - const provider = new LocalFileMetadataProvider( - "token.json", - "chain.json", - mockLogger, - mockCache, - ); - - const result = await provider.getTokensMetadata(); - - expect(result).toBe(mockTokenData); - expect(mockCache.get).toHaveBeenCalledWith("local-metadata-tokens"); - expect(fs.readFileSync).not.toHaveBeenCalled(); + expect( + () => new LocalFileMetadataProvider("token.json", "chain.json", mockLogger), + ).toThrow(InvalidSchema); }); - it("read and parse the token JSON file if the cached token data does not exist", async () => { - vi.spyOn(mockCache, "get").mockResolvedValue(undefined); - vi.spyOn(fs, "readFileSync").mockReturnValue(JSON.stringify(mockTokenData)); + 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, - mockCache, - ); - - const result = await provider.getTokensMetadata(); + const provider = new LocalFileMetadataProvider("token.json", "chain.json", mockLogger); + const chainMetadata = await provider.getChainsMetadata(); + const tokenMetadata = await provider.getTokensMetadata(); - expect(result).toEqual(mockTokenData); + 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"); - expect(mockCache.set).toHaveBeenCalledWith("local-metadata-tokens", mockTokenData); - }); - - it("throws an error if schema validation fails", 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(mockCache, "get").mockResolvedValue(undefined); - vi.spyOn(fs, "readFileSync").mockReturnValue(JSON.stringify(invalidTokenData)); - vi.spyOn(fs, "existsSync").mockReturnValue(true); - - const provider = new LocalFileMetadataProvider( - "token.json", - "chain.json", - mockLogger, - mockCache, - ); - - await expect(provider.getTokensMetadata()).rejects.toThrow(InvalidSchema); }); }); });