Skip to content
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: ecosystem configuration #61

Merged
merged 5 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,6 @@ contracts/out
contracts/cache

# Turborepo
.turbo
.turbo

.tmp
7 changes: 7 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
PORT=3000 # Port to run the API server on

ENVIRONMENT="" # Environment: 'mainnet' | 'testnet' | 'local'

BRIDGE_HUB_ADDRESS=""
SHARED_BRIDGE_ADDRESS=""
STATE_MANAGER_ADDRESSES="" #CSV list of State managers addresses

L1_RPC_URLS="" #CSV list of L1 RPC URLs
L2_RPC_URLS="" #CSV list of L2 RPC URLs

PRICING_SOURCE="dummy" # Pricing source: 'dummy' | 'coingecko'

DUMMY_PRICE="1" # Dummy price for the 'dummy' pricing source (optional)

# CoinGecko API
COINGECKO_API_KEY='' # CoinGecko API key
COINGECKO_BASE_URL='' # CoinGecko API base URL for the API version you are using
COINGECKO_API_TYPE='' # CoinGecko API Type: 'demo' or 'pro'
Expand Down
48 changes: 34 additions & 14 deletions apps/api/src/common/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import dotenv from "dotenv";
import { Address } from "viem";
import { mainnet, zksync } from "viem/chains";
import { localhost, mainnet, sepolia } 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,31 +42,50 @@ 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,
};
}
};

const getChain = (environment: "mainnet" | "testnet" | "local") => {
switch (environment) {
case "mainnet":
return mainnet;
case "testnet":
return sepolia;
case "local":
return localhost;
}
};

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

l1: {
rpcUrls: envData.L1_RPC_URLS,
chain: mainnet,
chain: getChain(envData.ENVIRONMENT),
},
l2:
envData.L2_RPC_URLS.length > 0
? {
rpcUrls: envData.L2_RPC_URLS,
chain: zksync,
}
: undefined,
bridgeHubAddress: envData.BRIDGE_HUB_ADDRESS as Address,
sharedBridgeAddress: envData.SHARED_BRIDGE_ADDRESS as Address,
stateTransitionManagerAddresses: envData.STATE_MANAGER_ADDRESSES as Address[],
pricing: {
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
42 changes: 29 additions & 13 deletions apps/api/src/common/config/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,11 @@ const baseSchema = z.object({
BRIDGE_HUB_ADDRESS: addressSchema,
SHARED_BRIDGE_ADDRESS: addressSchema,
STATE_MANAGER_ADDRESSES: addressArraySchema,
ENVIRONMENT: z.enum(["mainnet", "testnet", "local"]).default("mainnet"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to indicate that mainnet is the default somewhere in the docs? (.env.example, README, etc)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do it in another PR where I attack all documentation and read me detailed 🫡

L1_RPC_URLS: urlArraySchema,
L2_RPC_URLS: z
.union([z.literal(""), urlArraySchema])
.optional()
.transform((val) => {
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 +71,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";
5 changes: 4 additions & 1 deletion packages/pricing/src/providers/dummy.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { IPricingProvider, PriceResponse } from "../internal.js";
* This provider returns a fixed price of 1 for each token address.
*/
export class DummyPricingProvider implements IPricingProvider {
constructor(private readonly dummyPrice: number | undefined = undefined) {}
async getTokenPrices(addresses: Address[]): Promise<PriceResponse> {
return Promise.resolve(Object.fromEntries(addresses.map((address) => [address, 1])));
return Promise.resolve(
Object.fromEntries(addresses.map((address) => [address, this.dummyPrice])),
);
}
}
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");
});
});
Loading