-
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: dummy pricing provider #32
Changes from all commits
99eb985
759c2fb
11d8da6
68981e2
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 |
---|---|---|
|
@@ -14,7 +14,7 @@ const stringToJSONSchema = z.string().transform((str, ctx): z.infer<ReturnType<t | |
} | ||
}); | ||
|
||
const validationSchema = z.object({ | ||
const baseSchema = z.object({ | ||
RPC_URLS: stringToJSONSchema.pipe(z.array(z.string().url())), | ||
CHAIN_ID: z.coerce.number().int().positive(), | ||
FETCH_LIMIT: z.coerce.number().int().positive().default(500), | ||
|
@@ -23,13 +23,38 @@ const validationSchema = z.object({ | |
DATABASE_SCHEMA: z.string().default("public"), | ||
INDEXER_GRAPHQL_URL: z.string().url(), | ||
INDEXER_ADMIN_SECRET: z.string(), | ||
COINGECKO_API_KEY: z.string(), | ||
COINGECKO_API_TYPE: z.enum(["demo", "pro"]).default("pro"), | ||
PRICING_SOURCE: z.enum(["dummy", "coingecko"]).default("coingecko"), | ||
IPFS_GATEWAYS_URL: stringToJSONSchema | ||
.pipe(z.array(z.string().url())) | ||
.default('["https://ipfs.io"]'), | ||
}); | ||
|
||
const dummyPricingSchema = baseSchema.extend({ | ||
PRICING_SOURCE: z.literal("dummy"), | ||
DUMMY_PRICE: z.coerce.number().optional().default(1), | ||
}); | ||
|
||
const coingeckoPricingSchema = baseSchema.extend({ | ||
PRICING_SOURCE: z.literal("coingecko"), | ||
COINGECKO_API_KEY: z.string().min(1), | ||
COINGECKO_API_TYPE: z.enum(["demo", "pro"]).default("pro"), | ||
}); | ||
|
||
const validationSchema = z | ||
.discriminatedUnion("PRICING_SOURCE", [dummyPricingSchema, coingeckoPricingSchema]) | ||
.transform((val) => { | ||
if (val.PRICING_SOURCE === "dummy") { | ||
return { pricingSource: val.PRICING_SOURCE, dummyPrice: val.DUMMY_PRICE, ...val }; | ||
} | ||
|
||
return { | ||
pricingSource: val.PRICING_SOURCE, | ||
apiKey: val.COINGECKO_API_KEY, | ||
apiType: val.COINGECKO_API_TYPE, | ||
...val, | ||
}; | ||
}); | ||
|
||
Comment on lines
+32
to
+57
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 move this into a different file |
||
const env = validationSchema.safeParse(process.env); | ||
|
||
if (!env.success) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export class InvalidPricingSource extends Error { | ||
constructor() { | ||
super(`Invalid pricing source`); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export class MissingDependencies extends Error { | ||
constructor() { | ||
super(`Missing dependencies`); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,20 @@ | ||
export type { TokenPrice, IPricingProvider } from "./internal.js"; | ||
|
||
export { CoingeckoProvider } from "./internal.js"; | ||
export { CoingeckoProvider, DummyPricingProvider } from "./internal.js"; | ||
|
||
export { PricingProviderFactory } from "./internal.js"; | ||
export type { | ||
PricingConfig, | ||
PricingProvider, | ||
DummyPricingConfig, | ||
CoingeckoPricingConfig, | ||
} from "./internal.js"; | ||
|
||
export { | ||
UnsupportedChainException, | ||
NetworkException, | ||
UnknownPricingException, | ||
UnsupportedToken, | ||
InvalidPricingSource, | ||
MissingDependencies, | ||
} from "./internal.js"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { ILogger } from "@grants-stack-indexer/shared"; | ||
|
||
import { | ||
CoingeckoProvider, | ||
DummyPricingProvider, | ||
InvalidPricingSource, | ||
IPricingProvider, | ||
MissingDependencies, | ||
PricingConfig, | ||
PricingProvider, | ||
} from "../internal.js"; | ||
|
||
/** | ||
* Factory class for creating pricing providers. | ||
*/ | ||
export class PricingProviderFactory { | ||
/** | ||
* Creates a pricing provider based on the provided configuration. | ||
* @param options - The pricing configuration. | ||
* @param deps - dependencies to inject into the pricing provider. | ||
* @returns The created pricing provider. | ||
* @throws {InvalidPricingSource} if the pricing source is invalid. | ||
* @throws {MissingDependencies} if the dependencies are missing. | ||
*/ | ||
static create( | ||
options: PricingConfig<PricingProvider>, | ||
deps?: { | ||
logger?: ILogger; | ||
}, | ||
): IPricingProvider { | ||
let pricingProvider: IPricingProvider; | ||
|
||
switch (options.pricingSource) { | ||
case "dummy": | ||
pricingProvider = new DummyPricingProvider(options.dummyPrice); | ||
break; | ||
case "coingecko": | ||
if (!deps?.logger) { | ||
throw new MissingDependencies(); | ||
} | ||
Comment on lines
+38
to
+40
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'm wondering if we should standardize the dependencies validation with some kind of generic pattern (with the help of zod maybe?) while initializing objects. 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. you mean inside constructors or having a zod schema here instead of 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. Zod schema could be a solution yeah. I'm thinking in general, whenever a service needs dependencies and those dependencies might need to accomodate themselves to some particular schema (like optional dependencies, some dependencies need to be present based on some other dependency value, etc) I guess that applying a zod schema could cover 99% of the cases, I'll apply it in my code and I'll let you know how happy/sad it is to use it lol |
||
|
||
pricingProvider = new CoingeckoProvider( | ||
{ | ||
apiKey: options.apiKey, | ||
apiType: options.apiType, | ||
}, | ||
deps.logger, | ||
); | ||
break; | ||
default: | ||
throw new InvalidPricingSource(); | ||
} | ||
|
||
return pricingProvider; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from "./pricing.interface.js"; | ||
export * from "./pricingConfig.interface.js"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { PricingProvider } from "./index.js"; | ||
|
||
export type DummyPricingConfig = { | ||
pricingSource: "dummy"; | ||
dummyPrice?: number; | ||
}; | ||
|
||
export type CoingeckoPricingConfig = { | ||
pricingSource: "coingecko"; | ||
apiKey: string; | ||
apiType: "demo" | "pro"; | ||
}; | ||
|
||
export type PricingConfig<Source extends PricingProvider> = Source extends "dummy" | ||
? DummyPricingConfig | ||
: Source extends "coingecko" | ||
? CoingeckoPricingConfig | ||
: never; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { TokenCode } from "@grants-stack-indexer/shared"; | ||
|
||
import { IPricingProvider, TokenPrice } from "../internal.js"; | ||
|
||
/** | ||
* DummyPricingProvider class that implements the IPricingProvider interface. | ||
* This provider returns a configurable fixed price (defaults to 1) for any token code. | ||
* Used primarily for testing purposes when actual token prices are not needed. | ||
*/ | ||
export class DummyPricingProvider implements IPricingProvider { | ||
constructor(private readonly dummyPrice: number = 1) {} | ||
|
||
/* @inheritdoc */ | ||
async getTokenPrice( | ||
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. Extremely minor detail but the 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. ooo didn't know that |
||
_tokenCode: TokenCode, | ||
startTimestampMs: number, | ||
_endTimestampMs?: number, | ||
): Promise<TokenPrice | undefined> { | ||
return Promise.resolve({ | ||
priceUsd: this.dummyPrice, | ||
timestampMs: startTimestampMs, | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from "./coingecko.provider.js"; | ||
export * from "./dummy.provider.js"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { describe, expect, it } from "vitest"; | ||
|
||
import { ILogger } from "@grants-stack-indexer/shared"; | ||
|
||
import { CoingeckoProvider, PricingConfig } from "../../src/external.js"; | ||
import { PricingProviderFactory } from "../../src/factory/index.js"; | ||
import { InvalidPricingSource, MissingDependencies } from "../../src/internal.js"; | ||
import { DummyPricingProvider } from "../../src/providers/dummy.provider.js"; | ||
|
||
describe("PricingProviderFactory", () => { | ||
it("create a DummyPricingProvider", () => { | ||
const options: PricingConfig<"dummy"> = { | ||
pricingSource: "dummy", | ||
dummyPrice: 1, | ||
}; | ||
|
||
const pricingProvider = PricingProviderFactory.create(options); | ||
expect(pricingProvider).toBeInstanceOf(DummyPricingProvider); | ||
const dummyProvider = pricingProvider as DummyPricingProvider; | ||
expect(dummyProvider["dummyPrice"]).toBe(1); | ||
}); | ||
|
||
it("create a CoingeckoProvider", () => { | ||
const options: PricingConfig<"coingecko"> = { | ||
pricingSource: "coingecko", | ||
apiKey: "some-api-key", | ||
apiType: "pro", | ||
}; | ||
|
||
const pricingProvider = PricingProviderFactory.create(options, { | ||
logger: {} as unknown as ILogger, | ||
}); | ||
|
||
expect(pricingProvider).toBeInstanceOf(CoingeckoProvider); | ||
}); | ||
|
||
it("throws if logger instance is not provided for CoingeckoProvider", () => { | ||
const options: PricingConfig<"coingecko"> = { | ||
pricingSource: "coingecko", | ||
apiKey: "some-api-key", | ||
apiType: "demo", | ||
}; | ||
|
||
expect(() => PricingProviderFactory.create(options)).toThrowError(MissingDependencies); | ||
}); | ||
|
||
it("should throw an error for invalid pricing source", () => { | ||
const options = { | ||
source: "invalid", | ||
}; | ||
|
||
expect(() => { | ||
PricingProviderFactory.create(options as unknown as PricingConfig<"dummy">); | ||
}).toThrowError(InvalidPricingSource); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { describe, expect, it } from "vitest"; | ||
|
||
import { TokenCode } from "@grants-stack-indexer/shared"; | ||
|
||
import { DummyPricingProvider } from "../../src/providers/dummy.provider.js"; | ||
|
||
describe("DummyPricingProvider", () => { | ||
it("return 1 for all token prices", async () => { | ||
const provider = new DummyPricingProvider(); | ||
|
||
const response = await provider.getTokenPrice("ETH" as TokenCode, 11111111); | ||
|
||
expect(response).toEqual({ | ||
priceUsd: 1, | ||
timestampMs: 11111111, | ||
}); | ||
}); | ||
}); |
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.
Is that
#coingecko
ok?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.
is # for comment, in next PR i'll add a whitespace