-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: local file metadata provider #56
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export class FileNotFound extends Error { | ||
constructor(path: string) { | ||
super(`File not found at path: ${path}`); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from "./invalidSchema.exception.js"; | ||
export * from "./fetchError.exception.js"; | ||
export * from "./fileNotFound.exception.js"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from "./githubMetadata.provider.js"; | ||
export * from "./staticMetadata.provider.js"; | ||
export * from "./localFileMetadata.provider.js"; |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i was wondering if we want to index the token list by address like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i thought about it too but i would leave that task for when we refactor the pricing service, so now everything still works as it is, wdyt? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { existsSync, readFileSync } from "fs"; | ||
import { z } from "zod"; | ||
|
||
import { | ||
Cache, | ||
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 local file metadata provider. | ||
*/ | ||
export class LocalFileMetadataProvider implements IMetadataProvider { | ||
/** | ||
* 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are the local JSON files going to be live-updated while the app is running? If not, do we need the cache for local files? Wdyt about using two instance variables and reading the file contents inside this constructor? Something along these lines: constructor() {
if (!existsSync(...)) {
...
}
if (!existsSync(...)) {
...
}
this.chainsMetadata = readMetadata()
this.tokensMetadata = tokensMetadata()
}
async getChainsMetadata() {
return Promise.resolve(this.chainsMetadata);
} If for some business rule this provider needs to keep reading the local file from time to time (or if there's any other reason) dismiss this comment. 😎 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we had the exact same discussion with @0xkenj1 jajajaj, we ended up agreeing in make it work like the GithubMetadata as a matter of consistency There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can reconsider this 😅 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think i didn't consider the case in which u redeploy the chainhub instance, if you keep redis running and you had updated the JSON file, then it will pick the data from redis instead of the updated file. (Supposing infinite ttl ) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we'll go with this option ser, makes sense |
||
) { | ||
if (!existsSync(tokenJsonPath)) { | ||
throw new FileNotFound(tokenJsonPath); | ||
} | ||
|
||
if (!existsSync(chainJsonPath)) { | ||
throw new FileNotFound(chainJsonPath); | ||
} | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lets add natspec on |
||
async getChainsMetadata(): Promise<ZKChainMetadata> { | ||
let cachedData = await this.cache.get<ZKChainMetadata>( | ||
`${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<bigint, ZKChainMetadataItem>()); | ||
|
||
await this.cache.set(`${LOCALFILE_METADATA_PREFIX}-chains`, cachedData); | ||
} | ||
|
||
return cachedData; | ||
} | ||
|
||
async getTokensMetadata(): Promise<Token<TokenType>[]> { | ||
let cachedData = await this.cache.get<Token<TokenType>[]>( | ||
`${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); | ||
|
||
if (!validatedData.success) { | ||
this.logger.error(`Invalid Tokens metadata: ${validatedData.error.errors}`); | ||
throw new InvalidSchema("Invalid Tokens metadata"); | ||
} | ||
|
||
cachedData = validatedData.data; | ||
|
||
await this.cache.set(`${LOCALFILE_METADATA_PREFIX}-tokens`, cachedData); | ||
} | ||
|
||
return cachedData; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we use some sort of unique id here ? like UUID?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any suggestions here ? @0xyaco
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess there would be a possibility of, given a shared
Cache
instance between different classes, to overwrite a cached value by accident if someone uses the same key in multiple classes.I can't come up with anything simple as a workaround to that issue right now lol so I think we can keep it as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
an alternative im thinking now would be a wrapper around the cache, like BrandedCache or smth like that:
and that this class wraps the cache methods and automatically adds the prefix to the key
but seems too much at this point
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that it's too much, let's just be aware of this issue when defining cache keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets keep it as it is nigiri, no need for uuid