export type DerivePolicy = "copy" | "manual" | "computed"; export interface ParameterSchema { type?: string; format?: string; required?: string[]; properties?: Record; items?: ParameterSchema; [key: string]: unknown; } export interface DerivedReleaseParameters { values: Record; missing: string[]; computed: string[]; } export interface Create2MiningRequest { requestId: string; factory: string; initCodeHash: string; leadingZeroBytes: number; [key: string]: unknown; } export interface Create2MiningResult { factory: string; initCodeHash: string; leadingZeroBytes: number; salt: string; expectedAddress: string; } export type Create2ComputedResolution = | { status: "reused"; salt: string; expectedAddress: string } | { status: "mining_required"; request: Create2MiningRequest }; interface TaskParameters { params?: Record>; } const isObject = (value: unknown): value is Record => { return !!value && typeof value === "object" && !Array.isArray(value); }; const cloneValue = (value: unknown): unknown => { if (Array.isArray(value)) return value.map((item) => cloneValue(item)); if (isObject(value)) { return Object.fromEntries( Object.entries(value).map(([key, item]) => [key, cloneValue(item)]), ); } return value; }; const validateDerivePolicies = (schema: ParameterSchema, parentPath = ""): void => { for (const [key, propertySchema] of Object.entries(schema.properties || {})) { const fieldPath = parentPath ? parentPath + "." + key : key; const policy = propertySchema["x-gateflow-derive"]; if (policy !== "copy" && policy !== "manual" && policy !== "computed") { throw new Error( "Parameter " + fieldPath + " requires x-gateflow-derive: copy | manual | computed", ); } validateDerivePolicies(propertySchema, fieldPath); } }; export const deriveReleaseParameters = ( schema: ParameterSchema, developmentValues: Record, ): DerivedReleaseParameters => { validateDerivePolicies(schema); const properties = schema.properties || {}; const required = new Set(schema.required || []); const values: Record = {}; const missing: string[] = []; const computed: string[] = []; for (const [key, propertySchema] of Object.entries(properties)) { const policy = propertySchema["x-gateflow-derive"]; if (policy === "copy") { if (Object.prototype.hasOwnProperty.call(developmentValues, key)) { values[key] = cloneValue(developmentValues[key]); } else if (required.has(key)) { missing.push(key); } continue; } if (policy === "manual") { missing.push(key); continue; } computed.push(key); } return { values, missing, computed }; }; const isHexBytes = (value: unknown, bytes: number): value is string => { return typeof value === "string" && new RegExp("^0x[0-9a-fA-F]{" + (bytes * 2) + "}$").test(value); }; const equalHex = (left: string, right: string): boolean => { return left.toLowerCase() === right.toLowerCase(); }; const validLeadingZeroPolicy = (value: number): boolean => { return Number.isInteger(value) && value >= 0 && value <= 20; }; export const resolveCreate2ComputedParameter = ( source: Create2MiningResult, target: Create2MiningRequest, ): Create2ComputedResolution => { if (!isHexBytes(target.factory, 20)) { throw new Error("CREATE2 mining request factory must be an EVM address"); } if (!isHexBytes(target.initCodeHash, 32)) { throw new Error("CREATE2 mining request initCodeHash must be bytes32"); } if (!validLeadingZeroPolicy(target.leadingZeroBytes)) { throw new Error("CREATE2 mining request leadingZeroBytes must be an integer from 0 to 20"); } const expectedPrefix = "0x" + "00".repeat(target.leadingZeroBytes); const reusable = isHexBytes(source.factory, 20) && isHexBytes(source.initCodeHash, 32) && isHexBytes(source.salt, 32) && isHexBytes(source.expectedAddress, 20) && validLeadingZeroPolicy(source.leadingZeroBytes) && equalHex(source.factory, target.factory) && equalHex(source.initCodeHash, target.initCodeHash) && source.leadingZeroBytes === target.leadingZeroBytes && source.expectedAddress.toLowerCase().startsWith(expectedPrefix); return reusable ? { status: "reused", salt: source.salt, expectedAddress: source.expectedAddress, } : { status: "mining_required", request: cloneValue(target) as Create2MiningRequest, }; }; const valueType = (value: unknown): string => { if (Array.isArray(value)) return "array"; if (value === null) return "null"; return typeof value; }; const validateValue = (value: unknown, schema: ParameterSchema, path: string): void => { if (value === undefined) return; if (schema.type === "object") { if (!isObject(value)) { throw new Error(path + " must be an object"); } const properties = schema.properties || {}; for (const requiredKey of schema.required || []) { if (!Object.prototype.hasOwnProperty.call(value, requiredKey) || value[requiredKey] === undefined) { throw new Error(path + "." + requiredKey + " is required"); } } for (const [key, nestedValue] of Object.entries(value)) { if (properties[key]) validateValue(nestedValue, properties[key], path + "." + key); } } else if (schema.type === "array") { if (!Array.isArray(value)) { throw new Error(path + " must be an array"); } if (schema.items) { value.forEach((item, index) => validateValue(item, schema.items as ParameterSchema, path + "[" + index + "]")); } } else if (schema.type === "integer") { if (typeof value !== "number" || !Number.isInteger(value)) { throw new Error(path + " must be an integer"); } } else if (schema.type === "number") { if (typeof value !== "number" || !Number.isFinite(value)) { throw new Error(path + " must be a number"); } } else if (schema.type && valueType(value) !== schema.type) { throw new Error(path + " must be a " + schema.type); } if (schema.format === "address") { if (typeof value !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(value)) { throw new Error(path + " must be an EVM address"); } } }; const validateOverrideKeys = ( overrides: Record, schema: ParameterSchema, path: string, ): void => { const properties = schema.properties || {}; for (const [key, value] of Object.entries(overrides)) { if (!Object.prototype.hasOwnProperty.call(properties, key)) { throw new Error("Unknown override key: " + (path ? path + "." : "") + key); } const propertySchema = properties[key]; if (isObject(value) && propertySchema.type === "object") { validateOverrideKeys(value, propertySchema, path ? path + "." + key : key); } } }; const mergeValues = ( baseline: Record, overrides: Record, ): Record => { const merged = cloneValue(baseline) as Record; for (const [key, override] of Object.entries(overrides)) { const current = merged[key]; merged[key] = isObject(current) && isObject(override) ? mergeValues(current, override) : cloneValue(override); } return merged; }; export const resolveExecutionParameters = ( parameters: TaskParameters, targetId: string, overrides: Record, schema: ParameterSchema, ): Record => { const allParams = parameters.params || {}; const networkParams = allParams[targetId]; if (!networkParams) { throw new Error("Missing parameters for target " + targetId); } const baseline = { ...cloneValue(networkParams) as Record, global: cloneValue(allParams.global || {}), }; validateOverrideKeys(overrides, schema, ""); const resolved = mergeValues(baseline, overrides); validateValue(resolved, { ...schema, type: schema.type || "object" }, "parameters"); return resolved; };