mirror of
https://github.com/0xbow-io/privacy-pools-core.git
synced 2026-01-10 09:58:00 -05:00
feat: relayer config rework uses zod for config.json parsing (#65)
Removed environment variable parsing in favour of parsing a simple json file.
This commit is contained in:
15
packages/relayer/config.example.json
Normal file
15
packages/relayer/config.example.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"fee_receiver_address": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
|
||||
"provider_url": "http://0.0.0.0:8545",
|
||||
"fee_bps": "1000",
|
||||
"signer_private_key": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
|
||||
"sqlite_db_path": "/tmp/pp_relayer.sqlite",
|
||||
"entrypoint_address": "0xa513e6e4b8f2a923d98304ec87f64353c4d5c853",
|
||||
"chain": {
|
||||
"name": "localhost",
|
||||
"id": "31337"
|
||||
},
|
||||
"withdraw_amounts": {
|
||||
"0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9": 100
|
||||
}
|
||||
}
|
||||
@@ -5,20 +5,9 @@ services:
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: packages/relayer/Dockerfile
|
||||
environment:
|
||||
FEE_RECEIVER_ADDRESS: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
|
||||
PROVIDER_URL: http://0.0.0.0:8545
|
||||
FEE_BPS: 1000
|
||||
SIGNER_PRIVATE_KEY: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
|
||||
SQLITE_DB_PATH: /tmp/pp_relayer.sqlite
|
||||
CONFIG_PATH: ./withdraw_amounts.example.json
|
||||
ENTRYPOINT_ADDRESS: 0xa513E6E4b8f2a923D98304ec87F64353C4D5C853
|
||||
POOL_ADDRESS: 0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6
|
||||
CHAIN: localhost
|
||||
CHAIN_ID: 31337
|
||||
volumes:
|
||||
- /tmp/pp_relayer.sqlite:/pp_relayer.sqlite
|
||||
- ./withdraw_amounts.example.json:/build/packages/relayer/withdraw_amounts.example.json
|
||||
- ./config.example.json:/build/packages/relayer/config.json
|
||||
- ../circuits/artifacts:/build/node_modules/@privacy-pool-core/sdk/dist/node/artifacts
|
||||
ports:
|
||||
- "3000:3000" # HOST:CONTAINER
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# env example using anvil setup
|
||||
export FEE_RECEIVER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
|
||||
export PROVIDER_URL=http://127.0.0.1:8545
|
||||
export FEE_BPS=1000
|
||||
export SIGNER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
|
||||
export SQLITE_DB_PATH=/tmp/pp_relayer.sqlite
|
||||
export CONFIG_PATH=./withdraw_amounts.example.json
|
||||
export ENTRYPOINT_ADDRESS=0xa513E6E4b8f2a923D98304ec87F64353C4D5C853
|
||||
export POOL_ADDRESS=0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6
|
||||
export CHAIN=localhost
|
||||
export CHAIN_ID=31337
|
||||
@@ -37,7 +37,8 @@
|
||||
"express": "4.21.2",
|
||||
"sqlite": "5.1.1",
|
||||
"sqlite3": "5.1.7",
|
||||
"viem": "2.22.14"
|
||||
"viem": "2.22.14",
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "5.0.0"
|
||||
|
||||
@@ -1,152 +1,87 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { Address, Chain, defineChain, getAddress, Hex, isHex } from "viem";
|
||||
import { ConfigError } from "./exceptions/base.exception.js";
|
||||
import path from "node:path";
|
||||
import { defineChain, getAddress } from "viem";
|
||||
import { localhost, mainnet, sepolia } from "viem/chains";
|
||||
import { z } from "zod";
|
||||
import { ConfigError } from "./exceptions/base.exception.js";
|
||||
|
||||
const enum ConfigEnv {
|
||||
CONFIG_PATH = "CONFIG_PATH",
|
||||
FEE_RECEIVER_ADDRESS = "FEE_RECEIVER_ADDRESS",
|
||||
ENTRYPOINT_ADDRESS = "ENTRYPOINT_ADDRESS",
|
||||
PROVIDER_URL = "PROVIDER_URL",
|
||||
SIGNER_PRIVATE_KEY = "SIGNER_PRIVATE_KEY",
|
||||
FEE_BPS = "FEE_BPS",
|
||||
SQLITE_DB_PATH = "SQLITE_DB_PATH",
|
||||
CHAIN = "CHAIN",
|
||||
CHAIN_ID = "CHAIN_ID",
|
||||
}
|
||||
|
||||
type ConfigEnvString = `${ConfigEnv}`;
|
||||
|
||||
interface ConfigEnvVarChecker {
|
||||
(varNameValue: string): void;
|
||||
}
|
||||
|
||||
function checkConfigVar(
|
||||
varName: ConfigEnvString,
|
||||
checker?: ConfigEnvVarChecker,
|
||||
) {
|
||||
const varNameValue = process.env[varName];
|
||||
if (varNameValue === undefined) {
|
||||
throw ConfigError.default({
|
||||
context: `Environment variable \`${varName}\` is undefined`,
|
||||
});
|
||||
}
|
||||
if (checker) {
|
||||
try {
|
||||
checker(varNameValue);
|
||||
} catch (error) {
|
||||
if (error instanceof ConfigError) {
|
||||
throw error;
|
||||
} else {
|
||||
throw ConfigError.default({
|
||||
context: `Environment variable \`${varName}\` has an incorrect format`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return varNameValue;
|
||||
}
|
||||
|
||||
function checkHex(v: string) {
|
||||
if (!isHex(v, { strict: true })) {
|
||||
throw ConfigError.default({
|
||||
context: `String ${v} is not a properly formatted hex string`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getFeeReceiverAddress(): Address {
|
||||
return getAddress(
|
||||
checkConfigVar(ConfigEnv.FEE_RECEIVER_ADDRESS, (v) => getAddress(v)),
|
||||
);
|
||||
}
|
||||
|
||||
function getEntrypointAddress(): Address {
|
||||
return getAddress(
|
||||
checkConfigVar(ConfigEnv.ENTRYPOINT_ADDRESS, (v) => getAddress(v)),
|
||||
);
|
||||
}
|
||||
|
||||
function getProviderURL() {
|
||||
// TODO: check provider url format
|
||||
return checkConfigVar(ConfigEnv.PROVIDER_URL);
|
||||
}
|
||||
|
||||
function getSignerPrivateKey() {
|
||||
// TODO: check pk format
|
||||
return checkConfigVar(ConfigEnv.SIGNER_PRIVATE_KEY, checkHex) as Hex;
|
||||
}
|
||||
|
||||
function getFeeBps() {
|
||||
// TODO: check feeBPS format
|
||||
const feeBps = BigInt(checkConfigVar(ConfigEnv.FEE_BPS));
|
||||
// range validation
|
||||
if (feeBps > 10_000n || feeBps < 0) {
|
||||
throw ConfigError.feeBpsOutOfBounds();
|
||||
}
|
||||
return feeBps;
|
||||
}
|
||||
|
||||
function getSqliteDbPath() {
|
||||
// check path exists of warn of new one
|
||||
return checkConfigVar(ConfigEnv.SQLITE_DB_PATH, (v) => {
|
||||
const dbPath = path.resolve(v);
|
||||
if (!fs.existsSync(v)) {
|
||||
console.log("Creating new DB at", dbPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getMinWithdrawAmounts(): Record<string, bigint> {
|
||||
const envVar = checkConfigVar(ConfigEnv.CONFIG_PATH, (v) => {
|
||||
const configPath = path.resolve(v);
|
||||
if (!fs.existsSync(v)) {
|
||||
throw ConfigError.default({
|
||||
context: `${configPath} does not exist.`,
|
||||
});
|
||||
}
|
||||
});
|
||||
const withdrawAmountsRaw = JSON.parse(
|
||||
fs.readFileSync(path.resolve(envVar), { encoding: "utf-8" }),
|
||||
);
|
||||
const withdrawAmounts: Record<string, bigint> = {};
|
||||
for (const entry of Object.entries(withdrawAmountsRaw)) {
|
||||
const [asset, amount] = entry;
|
||||
if (typeof amount === "string" || typeof amount === "number") {
|
||||
withdrawAmounts[asset] = BigInt(amount);
|
||||
const zAddress = z
|
||||
.string()
|
||||
.regex(/^0x[0-9a-fA-F]+/)
|
||||
.length(42)
|
||||
.transform((v) => getAddress(v));
|
||||
const zPkey = z
|
||||
.string()
|
||||
.regex(/^0x[0-9a-fA-F]+/)
|
||||
.length(66)
|
||||
.transform((v) => v as `0x${string}`);
|
||||
const zChain = z
|
||||
.object({
|
||||
name: z.enum(["localhost", "mainnet", "sepolia"]),
|
||||
id: z
|
||||
.string()
|
||||
.or(z.number())
|
||||
.pipe(z.coerce.number())
|
||||
.refine((x) => x > 0)
|
||||
.default(31337),
|
||||
})
|
||||
.transform((c) => {
|
||||
if (c.name === "localhost") {
|
||||
return defineChain({ ...localhost, id: c.id });
|
||||
} else if (c.name === "sepolia") {
|
||||
return sepolia;
|
||||
} else if (c.name === "mainnet") {
|
||||
return mainnet;
|
||||
} else {
|
||||
console.error(`Unable to parse asset ${asset} with value ${amount}`);
|
||||
return z.NEVER;
|
||||
}
|
||||
});
|
||||
const zWithdrawAmounts = z.record(
|
||||
zAddress,
|
||||
z.number().nonnegative().pipe(z.coerce.bigint()),
|
||||
);
|
||||
const fee_bps = z
|
||||
.string()
|
||||
.or(z.number())
|
||||
.pipe(z.coerce.bigint().nonnegative().max(10_000n));
|
||||
|
||||
const configSchema = z
|
||||
.object({
|
||||
fee_receiver_address: zAddress,
|
||||
fee_bps: fee_bps,
|
||||
signer_private_key: zPkey,
|
||||
entrypoint_address: zAddress,
|
||||
provider_url: z.string().url(),
|
||||
chain: zChain,
|
||||
sqlite_db_path: z.string().transform((p) => path.resolve(p)),
|
||||
withdraw_amounts: zWithdrawAmounts,
|
||||
})
|
||||
.strict()
|
||||
.readonly();
|
||||
|
||||
function readConfigFile(): Record<string, unknown> {
|
||||
let configPathString = process.env["CONFIG_PATH"];
|
||||
if (!configPathString) {
|
||||
console.warn(
|
||||
"RELAYER_CONFIG is not set, using default path: ./config.json",
|
||||
);
|
||||
configPathString = "./config.json";
|
||||
}
|
||||
return withdrawAmounts;
|
||||
if (!fs.existsSync(configPathString)) {
|
||||
throw ConfigError.default("No config.json found for relayer.");
|
||||
}
|
||||
return JSON.parse(
|
||||
fs.readFileSync(path.resolve(configPathString), { encoding: "utf-8" }),
|
||||
);
|
||||
}
|
||||
|
||||
function getChainConfig(): Chain {
|
||||
const chainName = checkConfigVar(ConfigEnv.CHAIN);
|
||||
const chainId = process.env[ConfigEnv.CHAIN_ID];
|
||||
return ((chainNameValue) => {
|
||||
switch (chainNameValue) {
|
||||
case "localhost":
|
||||
if (chainId) {
|
||||
return defineChain({ ...localhost, id: Number(chainId) });
|
||||
}
|
||||
return localhost;
|
||||
case "sepolia":
|
||||
return sepolia;
|
||||
case "mainnet":
|
||||
return mainnet;
|
||||
default:
|
||||
throw ConfigError.chainNotSupported();
|
||||
}
|
||||
})(chainName);
|
||||
}
|
||||
const config = configSchema.parse(readConfigFile());
|
||||
|
||||
export const FEE_RECEIVER_ADDRESS = getFeeReceiverAddress();
|
||||
export const ENTRYPOINT_ADDRESS = getEntrypointAddress();
|
||||
export const PROVIDER_URL = getProviderURL();
|
||||
export const SIGNER_PRIVATE_KEY = getSignerPrivateKey();
|
||||
export const FEE_BPS = getFeeBps();
|
||||
export const SQLITE_DB_PATH = getSqliteDbPath();
|
||||
export const WITHDRAW_AMOUNTS = getMinWithdrawAmounts();
|
||||
export const CHAIN = getChainConfig();
|
||||
export const FEE_RECEIVER_ADDRESS = config.fee_receiver_address;
|
||||
export const ENTRYPOINT_ADDRESS = config.entrypoint_address;
|
||||
export const PROVIDER_URL = config.provider_url;
|
||||
export const SIGNER_PRIVATE_KEY = config.signer_private_key;
|
||||
export const FEE_BPS = config.fee_bps;
|
||||
export const SQLITE_DB_PATH = config.sqlite_db_path;
|
||||
export const WITHDRAW_AMOUNTS = config.withdraw_amounts;
|
||||
export const CHAIN = config.chain;
|
||||
|
||||
@@ -109,7 +109,7 @@ export class ConfigError extends RelayerError {
|
||||
constructor(
|
||||
message: string,
|
||||
code: ErrorCode = ErrorCode.INVALID_CONFIG,
|
||||
details?: Record<string, unknown>,
|
||||
details?: Record<string, unknown> | string,
|
||||
) {
|
||||
super(message, code, details);
|
||||
this.name = this.constructor.name;
|
||||
@@ -118,35 +118,11 @@ export class ConfigError extends RelayerError {
|
||||
/**
|
||||
* Creates an error for input validation failures.
|
||||
*/
|
||||
public static default(details?: Record<string, unknown>): ConfigError {
|
||||
public static default(
|
||||
details?: Record<string, unknown> | string,
|
||||
): ConfigError {
|
||||
return new ConfigError("Invalid config", ErrorCode.INVALID_CONFIG, details);
|
||||
}
|
||||
|
||||
public static feeBpsOutOfBounds(
|
||||
details?: Record<string, unknown>,
|
||||
): ConfigError {
|
||||
return new ConfigError(
|
||||
"Invalid config: FEE_BPS must be in range [0, 10_000]",
|
||||
ErrorCode.FEE_BPS_OUT_OF_BOUNDS,
|
||||
details,
|
||||
);
|
||||
}
|
||||
|
||||
public static chainNotSupported(
|
||||
details?: Record<string, unknown>,
|
||||
): ConfigError {
|
||||
return new ConfigError(
|
||||
"Invalid config: CHAIN must be one of `localhost`, `sepolia`, `mainnet`",
|
||||
ErrorCode.CHAIN_NOT_SUPPORTED,
|
||||
details,
|
||||
);
|
||||
}
|
||||
|
||||
public static override unknown(message: string): ConfigError {
|
||||
return new ConfigError("Invalid config", ErrorCode.INVALID_CONFIG, {
|
||||
context: `Unknown error for ${message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class WithdrawalValidationError extends RelayerError {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9": 100
|
||||
}
|
||||
@@ -6204,7 +6204,7 @@ yocto-queue@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110"
|
||||
integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==
|
||||
|
||||
zod@^3.21.4:
|
||||
zod@3.24.1, zod@^3.21.4:
|
||||
version "3.24.1"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee"
|
||||
integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==
|
||||
|
||||
Reference in New Issue
Block a user