From 0874782fd367d5dc6f4c5b5b88392d73323c2429 Mon Sep 17 00:00:00 2001 From: Aleksandr Pasevin Date: Mon, 1 Sep 2025 12:58:00 +0300 Subject: [PATCH 1/5] feat(ui): add BytesField component with validation support Implements a specialized BytesField component for blockchain bytes data: - Supports hex and base64 validation using validator.js library - EVM compatibility with configurable 0x prefix support - Built-in byte length validation and format detection - Comprehensive unit tests with 46 test cases - Integration with existing form field architecture - Chain-agnostic validation utilities in utils package # Conflicts: # packages/utils/src/index.ts # pnpm-lock.yaml --- .changeset/clear-trees-buy.md | 9 + .../utils/fieldTypeUtils.ts | 3 + .../builder/src/export/cli/export-app.cjs | 0 .../renderer/src/components/fieldRegistry.ts | 2 + packages/types/src/forms/fields.ts | 2 + .../ui/src/components/fields/BytesField.tsx | 203 + packages/ui/src/components/fields/index.ts | 1 + packages/utils/package.json | 4 +- .../src/__tests__/bytesValidation.test.ts | 335 ++ packages/utils/src/bytesValidation.ts | 214 + packages/utils/src/fieldDefaults.ts | 1 + packages/utils/src/index.ts | 1 + pnpm-lock.yaml | 3624 ++++++++--------- 13 files changed, 2583 insertions(+), 1816 deletions(-) create mode 100644 .changeset/clear-trees-buy.md mode change 100644 => 100755 packages/builder/src/export/cli/export-app.cjs create mode 100644 packages/ui/src/components/fields/BytesField.tsx create mode 100644 packages/utils/src/__tests__/bytesValidation.test.ts create mode 100644 packages/utils/src/bytesValidation.ts diff --git a/.changeset/clear-trees-buy.md b/.changeset/clear-trees-buy.md new file mode 100644 index 00000000..7fed9875 --- /dev/null +++ b/.changeset/clear-trees-buy.md @@ -0,0 +1,9 @@ +--- +'@openzeppelin/contracts-ui-builder-renderer': minor +'@openzeppelin/contracts-ui-builder-app': minor +'@openzeppelin/contracts-ui-builder-types': minor +'@openzeppelin/contracts-ui-builder-utils': minor +'@openzeppelin/contracts-ui-builder-ui': minor +--- + +support for new BytesField component with validation diff --git a/packages/builder/src/components/ContractsUIBuilder/StepFormCustomization/utils/fieldTypeUtils.ts b/packages/builder/src/components/ContractsUIBuilder/StepFormCustomization/utils/fieldTypeUtils.ts index 858b5511..3e8cef8a 100644 --- a/packages/builder/src/components/ContractsUIBuilder/StepFormCustomization/utils/fieldTypeUtils.ts +++ b/packages/builder/src/components/ContractsUIBuilder/StepFormCustomization/utils/fieldTypeUtils.ts @@ -27,6 +27,7 @@ export function getFieldTypeLabel(type: FieldType): string { const labelMap: Record = { text: 'Text Input', textarea: 'Text Area', + bytes: 'Bytes Input (Hex/Base64)', email: 'Email Input', password: 'Password Input', number: 'Number Input', @@ -61,6 +62,7 @@ const DEFAULT_FIELD_TYPES: FieldType[] = [ 'radio', 'select', 'textarea', + 'bytes', 'email', 'password', 'blockchain-address', @@ -100,6 +102,7 @@ export function getFieldTypeGroups( // Define field type categories const fieldTypeCategories: Record = { text: ['text', 'textarea', 'email', 'password'], + text: ['text', 'textarea', 'bytes', 'email', 'password'], numeric: ['number', 'amount'], selection: ['select', 'radio', 'checkbox'], blockchain: ['blockchain-address'], diff --git a/packages/builder/src/export/cli/export-app.cjs b/packages/builder/src/export/cli/export-app.cjs old mode 100644 new mode 100755 diff --git a/packages/renderer/src/components/fieldRegistry.ts b/packages/renderer/src/components/fieldRegistry.ts index fc7e0223..017ca74b 100644 --- a/packages/renderer/src/components/fieldRegistry.ts +++ b/packages/renderer/src/components/fieldRegistry.ts @@ -9,6 +9,7 @@ import { ArrayObjectField, BaseFieldProps, BooleanField, + BytesField, CodeEditorField, NumberField, ObjectField, @@ -38,6 +39,7 @@ export const fieldComponents: Record< select: SelectField, 'select-grouped': SelectGroupedField, textarea: TextAreaField, + bytes: BytesField, 'code-editor': CodeEditorField, date: () => React.createElement('div', null, 'Date field not implemented yet'), email: () => React.createElement('div', null, 'Email field not implemented yet'), diff --git a/packages/types/src/forms/fields.ts b/packages/types/src/forms/fields.ts index fad6952c..7c4b2aaf 100644 --- a/packages/types/src/forms/fields.ts +++ b/packages/types/src/forms/fields.ts @@ -17,6 +17,7 @@ export type FieldType = | 'radio' | 'select' | 'textarea' + | 'bytes' // Byte data with hex/base64 validation | 'code-editor' // Code editor with syntax highlighting | 'date' | 'email' @@ -38,6 +39,7 @@ export type FieldValue = T extends | 'email' | 'password' | 'textarea' + | 'bytes' | 'code-editor' | 'blockchain-address' ? string diff --git a/packages/ui/src/components/fields/BytesField.tsx b/packages/ui/src/components/fields/BytesField.tsx new file mode 100644 index 00000000..649c8873 --- /dev/null +++ b/packages/ui/src/components/fields/BytesField.tsx @@ -0,0 +1,203 @@ +import React from 'react'; +import { Controller, FieldValues } from 'react-hook-form'; + +import { validateBytesSimple } from '@openzeppelin/contracts-ui-builder-utils'; + +import { Label } from '../ui/label'; +import { Textarea } from '../ui/textarea'; +import { BaseFieldProps } from './BaseField'; +import { + ErrorMessage, + getAccessibilityProps, + getValidationStateClasses, + handleEscapeKey, +} from './utils'; + +/** + * BytesField component properties + */ +export interface BytesFieldProps + extends BaseFieldProps { + /** + * Number of rows for the textarea + */ + rows?: number; + + /** + * Maximum length in bytes (not characters) + */ + maxBytes?: number; + + /** + * Whether to accept hex, base64, or both formats + */ + acceptedFormats?: 'hex' | 'base64' | 'both'; + + /** + * Whether to automatically add/remove 0x prefix for hex values + */ + autoPrefix?: boolean; + + /** + * Whether to allow 0x prefix in hex input (defaults to true) + */ + allowHexPrefix?: boolean; +} + +/** + * Specialized input field for bytes data with built-in hex/base64 validation. + * + * This component provides proper validation for blockchain bytes data including: + * - Hex encoding validation (with optional 0x prefix support) + * - Base64 encoding validation + * - Byte length validation + * - Format detection and conversion + * + * Key props for EVM compatibility: + * - `allowHexPrefix`: Whether to accept 0x prefixed input (defaults to true) + * - `autoPrefix`: Whether to automatically add 0x prefixes (defaults to false) + * + * These are separate concerns - you can accept 0x input without auto-adding prefixes. + * + * Architecture flow: + * 1. Form schemas are generated from contract functions using adapters + * 2. TransactionForm renders the overall form structure with React Hook Form + * 3. DynamicFormField selects BytesField for 'bytes' field types + * 4. BaseField provides consistent layout and hook form integration + * 5. This component handles bytes-specific validation and formatting + */ +export function BytesField({ + id, + label, + helperText, + control, + name, + width = 'full', + validation, + placeholder = 'Enter hex or base64 encoded bytes', + rows = 3, + maxBytes, + acceptedFormats = 'both', + autoPrefix = false, + allowHexPrefix = true, + readOnly, +}: BytesFieldProps): React.ReactElement { + const isRequired = !!validation?.required; + const errorId = `${id}-error`; + const descriptionId = `${id}-description`; + + /** + * Validates bytes input format and encoding using validator.js + */ + const validateBytesField = (value: string): boolean | string => { + return validateBytesSimple(value, { + acceptedFormats, + maxBytes, + allowHexPrefix, // Allow prefix based on explicit prop (defaults to true) + }); + }; + + /** + * Formats the input value (adds 0x prefix if needed) + */ + const formatValue = (value: string): string => { + if (!value || !autoPrefix) return value; + + const cleanValue = value.trim().replace(/\s+/g, ''); + const withoutPrefix = cleanValue.startsWith('0x') ? cleanValue.slice(2) : cleanValue; + + // Only add prefix for valid hex that doesn't already have it + if (withoutPrefix && /^[0-9a-fA-F]*$/.test(withoutPrefix) && withoutPrefix.length % 2 === 0) { + return cleanValue.startsWith('0x') ? cleanValue : `0x${cleanValue}`; + } + + return cleanValue; + }; + + return ( +
+ {label && ( + + )} + + { + // Handle required validation explicitly + if (value === undefined || value === null || value === '') { + return validation?.required ? 'This field is required' : true; + } + + // Run bytes-specific validation using validator.js + return validateBytesField(value); + }, + }} + render={({ field, fieldState: { error } }) => { + const hasError = !!error; + const validationClasses = getValidationStateClasses(error); + + // Get accessibility attributes + const accessibilityProps = getAccessibilityProps({ + id, + hasError, + isRequired, + hasHelperText: !!helperText, + }); + + return ( + <> +