@openzeppelin/contracts-ui-builder-adapter-evm
Version:
EVM Adapter for Contracts UI Builder
168 lines (159 loc) • 8.39 kB
text/typescript
import type { AbiFunction, AbiParameter, AbiStateMutability } from 'viem';
import type {
ContractFunction,
ContractSchema,
FunctionParameter,
} from '@openzeppelin/contracts-ui-builder-types';
import { logger } from '@openzeppelin/contracts-ui-builder-utils';
import type { AbiItem } from '../types';
import { formatInputName, formatMethodName } from '../utils';
/**
* Transforms a standard ABI array (typically from an EVM-compatible chain)
* into the project's internal `ContractSchema` format.
* This schema is used by the builder app and renderer to represent contract interactions
* in a chain-agnostic way (though this specific transformer is for EVM ABIs).
*
* @param abi The raw ABI array (e.g., parsed from a JSON ABI file or fetched from Etherscan).
* It's expected to be an array of `AbiItem` (from viem types or a compatible structure).
* @param contractName A name to assign to the contract within the schema. This might be derived
* from a file name, user input, or a default if not otherwise available.
* @param address Optional address of the deployed contract. If provided, it's included in the schema.
* @returns A `ContractSchema` object representing the contract's interface.
*/
export function transformAbiToSchema(
abi: readonly AbiItem[],
contractName: string,
address?: string
): ContractSchema {
logger.info('transformAbiToSchema', `Transforming ABI to ContractSchema for: ${contractName}`);
const functions: ContractFunction[] = [];
for (const item of abi) {
// We are only interested in 'function' type items from the ABI
// to map them to our ContractFunction interface.
if (item.type === 'function') {
// After confirming item.type is 'function', we can safely cast it to AbiFunction
// to access function-specific properties like `stateMutability`, `inputs`, `outputs`.
const abiFunctionItem = item as AbiFunction;
functions.push({
// Generate a unique ID for the function within the schema.
// This often combines name and input types to handle overloads.
id: `${abiFunctionItem.name}_${abiFunctionItem.inputs?.map((i) => i.type).join('_') || ''}`,
name: abiFunctionItem.name || '', // Fallback for unnamed functions (though rare).
displayName: formatMethodName(abiFunctionItem.name || ''), // Create a more readable name for UI.
// Recursively map ABI inputs and outputs to our FunctionParameter structure.
// This ensures that any non-standard properties (like 'internalType') are stripped.
inputs: mapAbiParametersToSchemaParameters(abiFunctionItem.inputs),
outputs: mapAbiParametersToSchemaParameters(abiFunctionItem.outputs),
type: 'function', // Explicitly set, as we filtered for this type.
stateMutability: abiFunctionItem.stateMutability, // Preserve EVM-specific state mutability.
// Determine if the function modifies blockchain state based on its `stateMutability`.
// This is a crucial piece of information for the UI (e.g., to differentiate read vs. write calls).
modifiesState:
!abiFunctionItem.stateMutability || // If undefined, assume it modifies state (safer default)
!['view', 'pure'].includes(abiFunctionItem.stateMutability),
});
}
}
const contractSchema: ContractSchema = {
ecosystem: 'evm', // This transformer is specific to EVM.
name: contractName,
address,
functions,
};
logger.info(
'transformAbiToSchema',
`Transformation complete. Found ${contractSchema.functions.length} functions.`
);
return contractSchema;
}
/**
* Recursively maps an array of ABI parameters (from viem's `AbiParameter` type or compatible)
* to an array of `FunctionParameter` objects, which is our internal representation.
* This function is crucial for stripping any properties not defined in `FunctionParameter`
* (e.g., `internalType` from the raw ABI) and for handling nested components (structs/tuples).
*
* @param abiParams An array of ABI parameter objects. Can be undefined (e.g., if a function has no inputs/outputs).
* @returns An array of `FunctionParameter` objects, or an empty array if `abiParams` is undefined.
*/
function mapAbiParametersToSchemaParameters(
abiParams: readonly AbiParameter[] | undefined
): FunctionParameter[] {
if (!abiParams) {
return [];
}
return abiParams.map((param): FunctionParameter => {
// Create the base FunctionParameter object, picking only defined properties.
const schemaParam: FunctionParameter = {
name: param.name || '', // Ensure name is a string, fallback if undefined in ABI.
type: param.type, // The raw type string from the ABI (e.g., 'uint256', 'address', 'tuple').
displayName: formatInputName(param.name || '', param.type), // Generate a user-friendly name.
// `description` is not a standard part of an ABI parameter, so it's not mapped here.
// It can be added later by the user in the builder app UI.
};
// Check for nested components (structs/tuples).
// `param.type.startsWith('tuple')` checks if it's a tuple or tuple array.
// `'components' in param` is a type guard for discriminated unions.
// `param.components && param.components.length > 0` ensures components exist and are not empty.
if (
param.type.startsWith('tuple') &&
'components' in param && // Type guard for discriminated union (AbiParameter)
param.components &&
param.components.length > 0
) {
// If components exist, recursively call this function to map them.
// This ensures that nested structures also conform to `FunctionParameter` and strip extra fields.
// Cast `param.components` because TypeScript might not fully infer its type after the `in` check within the map.
schemaParam.components = mapAbiParametersToSchemaParameters(
param.components as readonly AbiParameter[]
);
}
return schemaParam;
});
}
/**
* Helper function to convert one of our internal `FunctionParameter` objects
* back into a format compatible with viem's `AbiParameter` type.
* This is primarily used by `createAbiFunctionItem` when constructing an `AbiFunction`
* for interactions with viem or other ABI-consuming libraries.
* It ensures that only properties expected by `AbiParameter` are included.
*
* @param param The internal `FunctionParameter` object.
* @returns An `AbiParameter` object compatible with viem.
*/
function mapSchemaParameterToAbiParameter(param: FunctionParameter): AbiParameter {
// Handle tuple types specifically, as `AbiParameter` for tuples requires a `components` array.
if (param.type.startsWith('tuple') && param.components && param.components.length > 0) {
return {
name: param.name || undefined, // ABI parameter names can be undefined (e.g., for return values).
type: param.type as `tuple${string}`, // Cast to satisfy viem's specific tuple type string.
// Recursively map nested components back to AbiParameter format.
components: param.components.map(mapSchemaParameterToAbiParameter),
};
}
// For non-tuple types, return a simpler AbiParameter structure.
return {
name: param.name || undefined,
type: param.type,
// `internalType` is not part of our `FunctionParameter` model, so it's not added back here.
// Other ABI-specific fields like `indexed` (for events) are also not relevant here as
// this function is focused on function parameters for `AbiFunction`.
};
}
/**
* Private helper to convert internal `ContractFunction` details (our model)
* back into a viem `AbiFunction` object.
* This is useful when interacting with libraries like viem that expect a standard ABI format.
* Ensures that the generated AbiFunction conforms to viem's type definitions.
*
* @param functionDetails The `ContractFunction` object from our internal schema.
* @returns An `AbiFunction` object.
*/
export function createAbiFunctionItem(functionDetails: ContractFunction): AbiFunction {
return {
name: functionDetails.name,
type: 'function',
inputs: functionDetails.inputs.map(mapSchemaParameterToAbiParameter),
outputs: functionDetails.outputs?.map(mapSchemaParameterToAbiParameter) || [],
stateMutability: (functionDetails.stateMutability ?? 'view') as AbiStateMutability,
};
}