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:
bezze
2025-02-06 06:45:53 -03:00
committed by GitHub
parent 822918d328
commit 599b2f62af
8 changed files with 101 additions and 199 deletions

View 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
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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;

View File

@@ -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 {

View File

@@ -1,3 +0,0 @@
{
"0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9": 100
}

View File

@@ -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==