Skip to content

Commit

Permalink
feat: pricing provider factory
Browse files Browse the repository at this point in the history
  • Loading branch information
0xnigir1 committed Aug 29, 2024
1 parent 5834cd0 commit 042cea1
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 22 deletions.
26 changes: 21 additions & 5 deletions apps/api/src/common/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Address } from "viem";
import { mainnet, zksync } from "viem/chains";

import { MetadataConfig } from "@zkchainhub/metadata";
import { PricingConfig } from "@zkchainhub/pricing";
import { Logger } from "@zkchainhub/shared";

import { validationSchema } from "./schemas.js";
Expand Down Expand Up @@ -41,8 +42,27 @@ const createMetadataConfig = (
}
};

const createPricingConfig = (env: typeof envData): PricingConfig<typeof env.PRICING_SOURCE> => {
switch (env.PRICING_SOURCE) {
case "dummy":
return {
source: "dummy",
dummyPrice: env.DUMMY_PRICE,
};
case "coingecko":
return {
source: "coingecko",
apiKey: env.COINGECKO_API_KEY,
apiBaseUrl: env.COINGECKO_BASE_URL,
apiType: env.COINGECKO_API_TYPE,
};
}
};

export const config = {
port: envData.PORT,
environment: envData.ENVIRONMENT,

l1: {
rpcUrls: envData.L1_RPC_URLS,
chain: mainnet,
Expand All @@ -61,11 +81,7 @@ export const config = {
cacheOptions: {
ttl: envData.CACHE_TTL,
},
pricingOptions: {
apiKey: envData.COINGECKO_API_KEY,
apiBaseUrl: envData.COINGECKO_BASE_URL,
apiType: envData.COINGECKO_API_TYPE,
},
...createPricingConfig(envData),
},
metadata: createMetadataConfig(envData),
} as const;
Expand Down
35 changes: 29 additions & 6 deletions apps/api/src/common/config/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const baseSchema = z.object({
BRIDGE_HUB_ADDRESS: addressSchema,
SHARED_BRIDGE_ADDRESS: addressSchema,
STATE_MANAGER_ADDRESSES: addressArraySchema,
ENVIRONMENT: z.enum(["mainnet", "testnet", "local"]).default("mainnet"),
L1_RPC_URLS: urlArraySchema,
L2_RPC_URLS: z
.union([z.literal(""), urlArraySchema])
Expand All @@ -31,7 +32,9 @@ const baseSchema = z.object({
if (val === undefined || val === "") return [];
return val;
}),
COINGECKO_API_KEY: z.string(),
PRICING_SOURCE: z.enum(["dummy", "coingecko"]).default("dummy"),
DUMMY_PRICE: z.coerce.number().optional(),
COINGECKO_API_KEY: z.string().optional(),
COINGECKO_BASE_URL: z.string().url().default("https://api.coingecko.com/api/v3/"),
COINGECKO_API_TYPE: z.enum(["demo", "pro"]).default("demo"),
CACHE_TTL: z.coerce.number().positive().default(60),
Expand Down Expand Up @@ -75,8 +78,28 @@ const staticSchema = baseSchema
METADATA_CHAIN_JSON_PATH: true,
});

export const validationSchema = z.discriminatedUnion("METADATA_SOURCE", [
githubSchema,
localSchema,
staticSchema,
]);
const dummyPricingSchema = baseSchema
.extend({
PRICING_SOURCE: z.literal("dummy"),
DUMMY_PRICE: z.coerce.number().optional(),
})
.omit({
COINGECKO_API_KEY: true,
COINGECKO_BASE_URL: true,
COINGECKO_API_TYPE: true,
});

const coingeckoPricingSchema = baseSchema
.extend({
PRICING_SOURCE: z.literal("coingecko"),
COINGECKO_API_KEY: z.string(),
COINGECKO_BASE_URL: z.string().url().default("https://api.coingecko.com/api/v3/"),
COINGECKO_API_TYPE: z.enum(["demo", "pro"]).default("demo"),
})
.omit({
DUMMY_PRICE: true,
});

export const validationSchema = z
.discriminatedUnion("METADATA_SOURCE", [githubSchema, localSchema, staticSchema])
.and(z.discriminatedUnion("PRICING_SOURCE", [dummyPricingSchema, coingeckoPricingSchema]));
14 changes: 5 additions & 9 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { caching } from "cache-manager";
import { EvmProvider } from "@zkchainhub/chain-providers";
import { MetadataProviderFactory } from "@zkchainhub/metadata";
import { L1MetricsService } from "@zkchainhub/metrics";
import { CoingeckoProvider } from "@zkchainhub/pricing";
import { PricingProviderFactory } from "@zkchainhub/pricing";
import { Logger } from "@zkchainhub/shared";

import { App } from "./app.js";
Expand All @@ -20,15 +20,11 @@ const main = async (): Promise<void> => {
});

const evmProvider = new EvmProvider(config.l1.rpcUrls, config.l1.chain, logger);
const pricingProvider = new CoingeckoProvider(
{
apiBaseUrl: config.pricing.pricingOptions.apiBaseUrl,
apiKey: config.pricing.pricingOptions.apiKey,
apiType: config.pricing.pricingOptions.apiType,
},
memoryCache,

const pricingProvider = PricingProviderFactory.create(config.pricing, {
cache: memoryCache,
logger,
);
});

const metadataProvider = MetadataProviderFactory.create(config.metadata, {
logger,
Expand Down
11 changes: 10 additions & 1 deletion packages/pricing/src/external.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
export type { IPricingProvider, PriceResponse } from "./internal.js";
export type {
IPricingProvider,
PriceResponse,
PricingConfig,
PricingProvider,
DummyPricingConfig,
CoingeckoPricingConfig,
} from "./internal.js";

export { RateLimitExceeded, ApiNotAvailable } from "./internal.js";

export { CoingeckoProvider, DummyPricingProvider } from "./internal.js";

export { PricingProviderFactory } from "./internal.js";
45 changes: 45 additions & 0 deletions packages/pricing/src/factory/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Cache, ILogger } from "@zkchainhub/shared";

import {
CoingeckoProvider,
DummyPricingProvider,
IPricingProvider,
PricingConfig,
PricingProvider,
} from "../internal.js";

export class PricingProviderFactory {
static create(
options: PricingConfig<PricingProvider>,
deps?: {
logger?: ILogger;
cache?: Cache;
},
): IPricingProvider {
let pricingProvider: IPricingProvider;

switch (options.source) {
case "dummy":
pricingProvider = new DummyPricingProvider(options.dummyPrice);
break;
case "coingecko":
if (!deps?.cache || !deps?.logger) {
throw new Error("Missing dependencies");
}
pricingProvider = new CoingeckoProvider(
{
apiBaseUrl: options.apiBaseUrl,
apiKey: options.apiKey,
apiType: options.apiType,
},
deps.cache,
deps.logger,
);
break;
default:
throw new Error("Invalid pricing source");
}

return pricingProvider;
}
}
1 change: 1 addition & 0 deletions packages/pricing/src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./pricing.interface.js";
export * from "./pricingConfig.interface.js";
2 changes: 1 addition & 1 deletion packages/pricing/src/interfaces/pricing.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Address } from "@zkchainhub/shared";
import { PriceResponse } from "../internal.js";

// providers
export type PricingProvider = "coingecko";
export type PricingProvider = "coingecko" | "dummy";

/**
* Represents a pricing service that retrieves token prices.
Expand Down
19 changes: 19 additions & 0 deletions packages/pricing/src/interfaces/pricingConfig.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { PricingProvider } from "./pricing.interface.js";

export interface DummyPricingConfig {
source: "dummy";
dummyPrice?: number;
}

export interface CoingeckoPricingConfig {
source: "coingecko";
apiKey: string;
apiBaseUrl: string;
apiType: "demo" | "pro";
}

export type PricingConfig<Source extends PricingProvider> = Source extends "dummy"
? DummyPricingConfig
: Source extends "coingecko"
? CoingeckoPricingConfig
: never;
1 change: 1 addition & 0 deletions packages/pricing/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from "./interfaces/index.js";
export * from "./exceptions/index.js";
export * from "./mappings/index.js";
export * from "./providers/index.js";
export * from "./factory/index.js";
83 changes: 83 additions & 0 deletions packages/pricing/test/unit/factory/pricingFactory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, expect, it } from "vitest";

import {
CoingeckoProvider,
DummyPricingProvider,
PricingConfig,
PricingProviderFactory,
} from "../../../src/internal.js";

describe("PricingProviderFactory", () => {
it("create a DummyPricingProvider", () => {
const options: PricingConfig<"dummy"> = {
source: "dummy",
dummyPrice: 1,
};

const pricingProvider = PricingProviderFactory.create(options);

expect(pricingProvider).toBeInstanceOf(DummyPricingProvider);
expect(pricingProvider["dummyPrice"]).toBe(1);
});

it("create a CoingeckoProvider", () => {
const options: PricingConfig<"coingecko"> = {
source: "coingecko",
apiKey: "some-api-key",
apiBaseUrl: "some-base-url",
apiType: "demo",
};

const pricingProvider = PricingProviderFactory.create(options, {
logger: {} as any,
cache: {} as any,
});

expect(pricingProvider).toBeInstanceOf(CoingeckoProvider);
expect(pricingProvider["options"]).toEqual({
apiKey: "some-api-key",
apiBaseUrl: "some-base-url",
apiType: "demo",
});
});

it("throws if cache instance is not provided for CoingeckoProvider", () => {
const options: PricingConfig<"coingecko"> = {
source: "coingecko",
apiKey: "some-api-key",
apiBaseUrl: "some-base-url",
apiType: "demo",
};

expect(() =>
PricingProviderFactory.create(options, {
logger: {} as any,
}),
).toThrowError("Missing dependencies");
});

it("throws if logger instance is not provided for CoingeckoProvider", () => {
const options: PricingConfig<"coingecko"> = {
source: "coingecko",
apiKey: "some-api-key",
apiBaseUrl: "some-base-url",
apiType: "demo",
};

expect(() =>
PricingProviderFactory.create(options, {
cache: {} as any,
}),
).toThrowError("Missing dependencies");
});

it("should throw an error for invalid pricing source", () => {
const options = {
source: "invalid",
};

expect(() => {
PricingProviderFactory.create(options as any);
}).toThrowError("Invalid pricing source");
});
});

0 comments on commit 042cea1

Please sign in to comment.