UNPKG

@patchworkdev/pdk

Version:

Patchwork Development Kit

162 lines (161 loc) 6.62 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.importProjectConfig = importProjectConfig; exports.exportProjectConfig = exportProjectConfig; exports.validatePatchworkProject = validatePatchworkProject; const chains_1 = require("viem/chains"); const plugins_1 = require("../../plugins"); function importProjectConfig(config) { const plugins = config.plugins?.map((pluginConfig) => loadPlugin(pluginConfig)) || []; const networks = { local: { chain: chains_1.anvil, rpc: 'http://anvil:8545', }, testnet: { chain: chains_1.baseSepolia, rpc: 'http://anvil:8545', }, mainnet: { chain: chains_1.base, rpc: 'http://anvil:8545', }, }; // Will return sane defaults if nothing specified if (config.networks) { for (const [key, networkConfig] of Object.entries(config.networks)) { if (key !== 'local' && key !== 'testnet' && key !== 'mainnet') { throw new Error(`Invalid network key: ${key}. Must be one of 'local', 'testnet', or 'mainnet'`); } networks[key] = loadNetwork(networkConfig); } } const project = { name: config.name, scopes: config.scopes, contracts: config.contracts, networks: networks, plugins: plugins, }; validatePatchworkProject(project); return project; } function exportProjectConfig(config) { const plugins = config.plugins.map((plugin) => { return { name: plugin.name, props: plugin.configProps }; }); const networks = { local: { chain: config.networks?.local.chain.name || '', rpc: config.networks?.local.rpc || '' }, testnet: { chain: config.networks?.testnet.chain.name || '', rpc: config.networks?.testnet.rpc || '' }, mainnet: { chain: config.networks?.mainnet.chain.name || '', rpc: config.networks?.mainnet.rpc || '' }, }; return { name: config.name, scopes: config.scopes, contracts: config.contracts, networks: networks, plugins: plugins, }; } function loadPlugin(config) { // TODO create a way to dynamically import plugins const pluginsMap = { ponder: (0, plugins_1.ponder)(config.props), react: (0, plugins_1.react)(config.props), }; const plugin = pluginsMap[config.name]; if (!plugin) { throw new Error(`Plugin ${config.name} not found`); } return plugin; } function loadNetwork(config) { // TODO update with all supported chains const chainMap = { anvil: chains_1.anvil, base: chains_1.base, baseSepolia: chains_1.baseSepolia, }; const chain = chainMap[config.chain]; if (!chain) { throw new Error(`Chain ${config.chain} not found`); } return { chain: chain, rpc: config.rpc }; } const RESERVED_WORDS = ['metadata']; function isAlphanumeric(str) { return /^[a-zA-Z0-9]+$/.test(str); } // Checks if string is an Ethereum address function isEthereumAddress(str) { return /^0x[a-fA-F0-9]{40}$/.test(str); } function validatePatchworkProject(project) { // Get all valid contract keys const contractKeys = Object.keys(project.contracts); // Validate project name is alphanumeric if (!isAlphanumeric(project.name)) { throw new Error(`Invalid project name "${project.name}": project name must contain only alphanumeric characters`); } // Validate scope references to contracts project.scopes.forEach((scope) => { // Validate bankers if (scope.bankers) { scope.bankers.forEach((banker) => { if (!isEthereumAddress(banker) && !contractKeys.includes(banker)) { throw new Error(`Invalid banker reference "${banker}" in scope "${scope.name}": must be an Ethereum address or a valid contract key`); } }); } // Validate operators if (scope.operators) { scope.operators.forEach((operator) => { if (!isEthereumAddress(operator) && !contractKeys.includes(operator)) { throw new Error(`Invalid operator reference "${operator}" in scope "${scope.name}": must be an Ethereum address or a valid contract key`); } }); } }); // Validate contracts Object.entries(project.contracts).forEach(([contractKey, contractConfig]) => { // Skip validation for string references if (typeof contractConfig !== 'object') return; // Validate contract name is alphanumeric if (!isAlphanumeric(contractConfig.name)) { throw new Error(`Invalid contract name "${contractConfig.name}": contract name must contain only alphanumeric characters`); } // Validate contract key matches contract name if (contractKey !== contractConfig.name) { throw new Error(`Contract key mismatch: the key "${contractKey}" must match the contract name "${contractConfig.name}". Please update either the contract key or the name field to match.`); } // Validate fragments if (contractConfig.fragments) { contractConfig.fragments.forEach((fragment) => { if (!contractKeys.includes(fragment)) { throw new Error(`Invalid fragment reference "${fragment}" in contract "${contractConfig.name}": must be a valid contract key`); } }); } // Validate that no field key is exactly a reserved word contractConfig.fields.forEach((field) => { RESERVED_WORDS.forEach((reserved) => { if (field.key === reserved) { throw new Error(`Invalid field key "${field.key}" in contract "${contractConfig.name}": field keys cannot be exactly the reserved word "${reserved}".`); } }); }); // Validate duplicate field keys const keys = contractConfig.fields.map((field) => field.key); const uniqueKeys = new Set(keys); if (uniqueKeys.size !== keys.length) { throw new Error(`Duplicate field keys found in contract "${contractConfig.name}".`); } // Validate duplicate field IDs const fieldIds = contractConfig.fields.map((field) => field.id); const uniqueIds = new Set(fieldIds); if (uniqueIds.size !== fieldIds.length) { throw new Error(`Duplicate field IDs found in contract "${contractConfig.name}".`); } }); }