Skip to content

Commit

Permalink
feat: ecosystem configuration (#61)
Browse files Browse the repository at this point in the history
# 🤖 Linear

Closes ZKS-211

## Description

- Configurable price for `DummyPricingProvider` (jic)
- `PricingProviderFactory`
- `ENVIRONMENT` variable to switch between mainnet, testnet or local
  • Loading branch information
0xnigir1 authored Aug 30, 2024
1 parent e977641 commit bbdd571
Show file tree
Hide file tree
Showing 14 changed files with 260 additions and 47 deletions.
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"),
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

0 comments on commit bbdd571

Please sign in to comment.