Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .changeset/bigint-field-type-implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
'@openzeppelin/ui-builder-types': minor
'@openzeppelin/ui-builder-ui': minor
'@openzeppelin/ui-builder-renderer': minor
'@openzeppelin/ui-builder-utils': minor
'@openzeppelin/ui-builder-adapter-evm': minor
'@openzeppelin/ui-builder-adapter-stellar': minor
'@openzeppelin/ui-builder-app': patch
---

Add BigInt field type for safe handling of large integers beyond JavaScript Number precision

**Breaking Behavior Changes:**

- Large integer types (64-bit and above) now map to `bigint` field type instead of `number`
- **EVM**: `uint64`, `uint128`, `uint256`, `int64`, `int128`, `int256`
- **Stellar**: `U64`, `U128`, `U256`, `I64`, `I128`, `I256`
- BigInt values are stored and transmitted as strings to prevent precision loss

**New Features:**

- Added new `BigIntField` component with built-in integer validation
- Added `createBigIntTransform()` for proper string-based value handling
- BigInt field now available in UI Builder field type dropdown under "Numeric" category

**Improvements:**

- Fixes uint256 truncation issue (#194)
- Prevents precision loss for values exceeding `Number.MAX_SAFE_INTEGER` (2^53-1)
- Simplified field generation by removing redundant type-specific validation logic from adapters
- Component-based validation ensures consistency across all blockchain ecosystems

**Technical Details:**

- `BigIntField` uses string storage to handle arbitrary-precision integers
- Integer-only validation via regex (`/^-?\d+$/`)
- Compatible field types properly ordered with `bigint` as recommended type
- Transform functions ensure safe conversion between UI and blockchain formats
100 changes: 100 additions & 0 deletions packages/adapter-evm/src/mapping/__tests__/field-generator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { describe, expect, it } from 'vitest';

import type { FunctionParameter } from '@openzeppelin/ui-builder-types';

import { generateEvmDefaultField } from '../field-generator';

// Helper to create a mock function parameter
const createParam = (type: string, name: string, description?: string): FunctionParameter => ({
name,
type,
displayName: name,
description,
});

describe('EVM Field Generator', () => {
describe('generateEvmDefaultField', () => {
it('should generate a number field for small integer types', () => {
const param = createParam('uint32', 'count');
const field = generateEvmDefaultField(param);

expect(field.type).toBe('number');
expect(field.validation.required).toBe(true);
expect(field.validation.pattern).toBeUndefined();
});

it('should generate a bigint field for uint128', () => {
const param = createParam('uint128', 'amount');
const field = generateEvmDefaultField(param);

expect(field.type).toBe('bigint');
expect(field.validation.required).toBe(true);
// BigIntField component handles its own validation and UI guidance
});

it('should generate a bigint field for uint256', () => {
const param = createParam('uint256', 'value');
const field = generateEvmDefaultField(param);

expect(field.type).toBe('bigint');
expect(field.validation.required).toBe(true);
// BigIntField component handles its own validation and UI guidance
});

it('should generate a bigint field for int128', () => {
const param = createParam('int128', 'delta');
const field = generateEvmDefaultField(param);

expect(field.type).toBe('bigint');
expect(field.validation.required).toBe(true);
// BigIntField component handles its own validation and UI guidance
});

it('should generate a bigint field for int256', () => {
const param = createParam('int256', 'offset');
const field = generateEvmDefaultField(param);

expect(field.type).toBe('bigint');
expect(field.validation.required).toBe(true);
// BigIntField component handles its own validation and UI guidance
});

it('should preserve parameter description in helper text', () => {
const param = createParam('uint256', 'tokenId', 'The unique identifier of the NFT');
const field = generateEvmDefaultField(param);

expect(field.type).toBe('bigint');
expect(field.helperText).toBe('The unique identifier of the NFT');
});

it('should generate a blockchain-address field for address type', () => {
const param = createParam('address', 'recipient');
const field = generateEvmDefaultField(param);

expect(field.type).toBe('blockchain-address');
expect(field.validation.required).toBe(true);
expect(field.validation.pattern).toBeUndefined();
});

it('should generate array field with proper element config for uint256[]', () => {
const param = createParam('uint256[]', 'amounts');
const field = generateEvmDefaultField(param);

expect(field.type).toBe('array');
expect(field.elementFieldConfig).toBeDefined();
expect(field.elementFieldConfig?.type).toBe('bigint');
// Integer validation is handled by BigIntField component internally
});

it('should include proper field metadata', () => {
const param = createParam('uint256', 'value');
const field = generateEvmDefaultField(param);

expect(field.id).toBeDefined();
expect(field.name).toBe('value');
expect(field.label).toBe('Value');
expect(field.placeholder).toContain('value');
expect(field.width).toBe('full');
});
});
});
139 changes: 139 additions & 0 deletions packages/adapter-evm/src/mapping/__tests__/type-mapper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { describe, expect, it } from 'vitest';

import { getEvmCompatibleFieldTypes, mapEvmParamTypeToFieldType } from '../type-mapper';

describe('EVM Type Mapper', () => {
describe('mapEvmParamTypeToFieldType', () => {
it('should map small integer types to number field', () => {
expect(mapEvmParamTypeToFieldType('uint')).toBe('number');
expect(mapEvmParamTypeToFieldType('uint8')).toBe('number');
expect(mapEvmParamTypeToFieldType('uint16')).toBe('number');
expect(mapEvmParamTypeToFieldType('uint32')).toBe('number');
expect(mapEvmParamTypeToFieldType('int')).toBe('number');
expect(mapEvmParamTypeToFieldType('int8')).toBe('number');
expect(mapEvmParamTypeToFieldType('int16')).toBe('number');
expect(mapEvmParamTypeToFieldType('int32')).toBe('number');
});

it('should map 64-bit and larger integer types to bigint field to avoid precision loss', () => {
// These types can hold values larger than JavaScript's Number.MAX_SAFE_INTEGER (2^53-1)
expect(mapEvmParamTypeToFieldType('uint64')).toBe('bigint');
expect(mapEvmParamTypeToFieldType('uint128')).toBe('bigint');
expect(mapEvmParamTypeToFieldType('uint256')).toBe('bigint');
expect(mapEvmParamTypeToFieldType('int64')).toBe('bigint');
expect(mapEvmParamTypeToFieldType('int128')).toBe('bigint');
expect(mapEvmParamTypeToFieldType('int256')).toBe('bigint');
});

it('should map address to blockchain-address field', () => {
expect(mapEvmParamTypeToFieldType('address')).toBe('blockchain-address');
});

it('should map bool to checkbox field', () => {
expect(mapEvmParamTypeToFieldType('bool')).toBe('checkbox');
});

it('should map string to text field', () => {
expect(mapEvmParamTypeToFieldType('string')).toBe('text');
});

it('should map bytes types to text/textarea fields', () => {
expect(mapEvmParamTypeToFieldType('bytes')).toBe('textarea');
expect(mapEvmParamTypeToFieldType('bytes32')).toBe('text');
});

it('should map array types correctly', () => {
expect(mapEvmParamTypeToFieldType('uint256[]')).toBe('array');
expect(mapEvmParamTypeToFieldType('address[]')).toBe('array');
expect(mapEvmParamTypeToFieldType('string[]')).toBe('array');
});

it('should map tuple types to object', () => {
expect(mapEvmParamTypeToFieldType('tuple')).toBe('object');
});

it('should map array of tuples to array-object', () => {
expect(mapEvmParamTypeToFieldType('tuple[]')).toBe('array-object');
expect(mapEvmParamTypeToFieldType('tuple[5]')).toBe('array-object');
});

it('should default to text for unknown types', () => {
expect(mapEvmParamTypeToFieldType('unknown')).toBe('text');
expect(mapEvmParamTypeToFieldType('custom')).toBe('text');
});
});

describe('getEvmCompatibleFieldTypes', () => {
it('should return bigint as first compatible type for 64-bit and larger unsigned integers', () => {
// Verify uint64
const compatibleTypesUint64 = getEvmCompatibleFieldTypes('uint64');
expect(compatibleTypesUint64[0]).toBe('bigint'); // First (recommended) type
expect(compatibleTypesUint64).toContain('number');
expect(compatibleTypesUint64).toContain('amount');
expect(compatibleTypesUint64).toContain('text');

// Verify uint128
const compatibleTypesUint128 = getEvmCompatibleFieldTypes('uint128');
expect(compatibleTypesUint128[0]).toBe('bigint'); // First (recommended) type
expect(compatibleTypesUint128).toContain('number');
expect(compatibleTypesUint128).toContain('amount');
expect(compatibleTypesUint128).toContain('text');

// Verify uint256
const compatibleTypesUint256 = getEvmCompatibleFieldTypes('uint256');
expect(compatibleTypesUint256[0]).toBe('bigint'); // First (recommended) type
expect(compatibleTypesUint256).toContain('number');
expect(compatibleTypesUint256).toContain('amount');
expect(compatibleTypesUint256).toContain('text');
});

it('should return bigint as first compatible type for 64-bit and larger signed integers', () => {
// Verify int64
const compatibleTypesInt64 = getEvmCompatibleFieldTypes('int64');
expect(compatibleTypesInt64[0]).toBe('bigint'); // First (recommended) type
expect(compatibleTypesInt64).toContain('number');
expect(compatibleTypesInt64).toContain('text');

// Verify int128
const compatibleTypesInt128 = getEvmCompatibleFieldTypes('int128');
expect(compatibleTypesInt128[0]).toBe('bigint'); // First (recommended) type
expect(compatibleTypesInt128).toContain('number');
expect(compatibleTypesInt128).toContain('text');

// Verify int256
const compatibleTypesInt256 = getEvmCompatibleFieldTypes('int256');
expect(compatibleTypesInt256[0]).toBe('bigint'); // First (recommended) type
expect(compatibleTypesInt256).toContain('number');
expect(compatibleTypesInt256).toContain('text');
});

it('should return number as first compatible type for small integers', () => {
// Small integers that fit within JavaScript Number precision
const compatibleTypes = getEvmCompatibleFieldTypes('uint32');
expect(compatibleTypes[0]).toBe('number'); // First (recommended) type
expect(compatibleTypes).toContain('amount');
expect(compatibleTypes).toContain('text');
});

it('should return compatible field types for address', () => {
const compatibleTypes = getEvmCompatibleFieldTypes('address');
expect(compatibleTypes[0]).toBe('blockchain-address'); // First (recommended) type
expect(compatibleTypes).toContain('text');
});

it('should return compatible field types for bool', () => {
const compatibleTypes = getEvmCompatibleFieldTypes('bool');
expect(compatibleTypes[0]).toBe('checkbox'); // First (recommended) type
expect(compatibleTypes).toContain('select');
expect(compatibleTypes).toContain('radio');
expect(compatibleTypes).toContain('text');
});

it('should return compatible field types for arrays', () => {
const compatibleTypes = getEvmCompatibleFieldTypes('uint256[]');
expect(compatibleTypes[0]).toBe('array'); // First (recommended) type
expect(compatibleTypes).toContain('textarea');
expect(compatibleTypes).toContain('text');
});
});
});
18 changes: 12 additions & 6 deletions packages/adapter-evm/src/mapping/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import type { FieldType } from '@openzeppelin/ui-builder-types';

/**
* EVM-specific type mapping to default form field types.
*
* Note: Large integer types (uint128, uint256, int128, int256) are mapped to 'bigint'
* instead of 'number' to avoid JavaScript's Number precision limitations.
* JavaScript's Number type can only safely represent integers up to 2^53 - 1,
* but these types can hold much larger values. The BigIntField component stores values
* as strings and the EVM adapter handles BigInt conversion automatically.
*/
export const EVM_TYPE_TO_FIELD_TYPE: Record<string, FieldType> = {
address: 'blockchain-address',
Expand All @@ -10,16 +16,16 @@ export const EVM_TYPE_TO_FIELD_TYPE: Record<string, FieldType> = {
uint8: 'number',
uint16: 'number',
uint32: 'number',
uint64: 'number',
uint128: 'number',
uint256: 'number',
uint64: 'bigint',
uint128: 'bigint',
uint256: 'bigint',
int: 'number',
int8: 'number',
int16: 'number',
int32: 'number',
int64: 'number',
int128: 'number',
int256: 'number',
int64: 'bigint',
int128: 'bigint',
int256: 'bigint',
bool: 'checkbox',
bytes: 'textarea',
bytes32: 'text',
Expand Down
11 changes: 6 additions & 5 deletions packages/adapter-evm/src/mapping/field-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ function extractArrayElementType(parameterType: string): string | null {
}

/**
* Get default validation rules for a parameter type.
* Only includes serializable validation rules - no custom functions.
* Get default validation rules for a parameter.
* Field-specific validation is handled by the field components themselves.
*/
function getDefaultValidationForType(): FieldValidation {
function getDefaultValidation(): FieldValidation {
return { required: true };
}

Expand All @@ -40,6 +40,7 @@ export function generateEvmDefaultField<T extends FieldType = FieldType>(
parameter: FunctionParameter
): FormFieldType<T> {
const fieldType = mapEvmParamTypeToFieldType(parameter.type) as T;

const baseField: FormFieldType<T> = {
id: `field-${Math.random().toString(36).substring(2, 9)}`,
name: parameter.name || parameter.type, // Use type if name missing
Expand All @@ -48,7 +49,7 @@ export function generateEvmDefaultField<T extends FieldType = FieldType>(
placeholder: `Enter ${parameter.displayName || parameter.name || parameter.type}`,
helperText: parameter.description || '',
defaultValue: getDefaultValueForType(fieldType) as FieldValue<T>,
validation: getDefaultValidationForType(),
validation: getDefaultValidation(),
width: 'full',
};

Expand All @@ -64,7 +65,7 @@ export function generateEvmDefaultField<T extends FieldType = FieldType>(
elementType: elementFieldType,
elementFieldConfig: {
type: elementFieldType,
validation: getDefaultValidationForType(),
validation: getDefaultValidation(),
placeholder: `Enter ${elementType}`,
},
};
Expand Down
12 changes: 6 additions & 6 deletions packages/adapter-evm/src/mapping/type-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,16 @@ export function getEvmCompatibleFieldTypes(parameterType: string): FieldType[] {
uint8: ['number', 'amount', 'text'],
uint16: ['number', 'amount', 'text'],
uint32: ['number', 'amount', 'text'],
uint64: ['number', 'amount', 'text'],
uint128: ['number', 'amount', 'text'],
uint256: ['number', 'amount', 'text'],
uint64: ['bigint', 'number', 'amount', 'text'],
uint128: ['bigint', 'number', 'amount', 'text'],
uint256: ['bigint', 'number', 'amount', 'text'],
int: ['number', 'text'],
int8: ['number', 'text'],
int16: ['number', 'text'],
int32: ['number', 'text'],
int64: ['number', 'text'],
int128: ['number', 'text'],
int256: ['number', 'text'],
int64: ['bigint', 'number', 'text'],
int128: ['bigint', 'number', 'text'],
int256: ['bigint', 'number', 'text'],
bool: ['checkbox', 'select', 'radio', 'text'],
string: ['text', 'textarea', 'email', 'password'],
bytes: ['textarea', 'text'],
Expand Down
Loading
Loading