@openzeppelin/contracts-ui-builder-adapter-stellar
Version:
Stellar Adapter for Contracts UI Builder
169 lines (152 loc) • 6.65 kB
text/typescript
import { Address, xdr } from '@stellar/stellar-sdk';
import type {
ContractSchema,
EnumValue,
FormFieldType,
FunctionParameter,
} from '@openzeppelin/contracts-ui-builder-types';
import { isEnumValue } from '@openzeppelin/contracts-ui-builder-types';
import { logger } from '@openzeppelin/contracts-ui-builder-utils';
import { extractEnumVariants, isEnumType } from '../mapping/enum-metadata';
import { parseStellarInput } from '../transform';
/**
* Stellar transaction data structure that will be passed to signAndBroadcast
*/
export interface StellarTransactionData {
contractAddress: string;
functionName: string;
args: unknown[];
argTypes: string[]; // Parameter types for nativeToScVal type hints
argSchema?: FunctionParameter[]; // Full parameter schema with struct field info
transactionOptions: Record<string, unknown>;
}
/**
* Formats transaction data for Stellar chains based on parsed inputs.
*
* @param contractSchema The contract schema.
* @param functionId The ID of the function being called.
* @param submittedInputs The raw data submitted from the form.
* @param fields The fields of the form schema.
* @returns The formatted data payload suitable for signAndBroadcast.
*/
export function formatStellarTransactionData(
contractSchema: ContractSchema,
functionId: string,
submittedInputs: Record<string, unknown>,
fields: FormFieldType[]
): StellarTransactionData {
logger.info(
'formatStellarTransactionData',
`Formatting Stellar transaction data for function: ${functionId}`
);
// --- Step 1: Determine Argument Order --- //
const functionDetails = contractSchema.functions.find((fn) => fn.id === functionId);
if (!functionDetails) {
throw new Error(`Function definition for ${functionId} not found in provided contract schema.`);
}
const expectedArgs = functionDetails.inputs;
// --- Step 2: Iterate and Select Values --- //
const orderedRawValues: unknown[] = [];
for (const expectedArg of expectedArgs) {
const fieldConfig = fields.find((field: FormFieldType) => field.name === expectedArg.name);
if (!fieldConfig) {
throw new Error(`Configuration missing for argument: ${expectedArg.name} in provided fields`);
}
let value: unknown;
if (fieldConfig.isHardcoded) {
// FIX: If hardcoded value is undefined but we have submitted input, use submitted input instead
// This handles cases where fields were incorrectly saved with undefined hardcoded values
if (fieldConfig.hardcodedValue === undefined && fieldConfig.name in submittedInputs) {
logger.warn(
'formatStellarTransactionData',
`Field '${fieldConfig.name}' is hardcoded with undefined value but has submitted input. Using submitted input instead.`
);
value = submittedInputs[fieldConfig.name];
} else {
value = fieldConfig.hardcodedValue;
}
} else if (fieldConfig.isHidden) {
throw new Error(`Field '${fieldConfig.name}' cannot be hidden without being hardcoded.`);
} else {
if (!(fieldConfig.name in submittedInputs)) {
throw new Error(`Missing submitted input for required field: ${fieldConfig.name}`);
}
value = submittedInputs[fieldConfig.name];
}
orderedRawValues.push(value);
}
// --- Step 3: Parse/Transform Values using the imported parser --- //
const transformedArgs = expectedArgs.map((param, index) => {
let valueToParse = orderedRawValues[index];
// Handle enum values - process payload types using enum metadata
if (isEnumValue(valueToParse)) {
const specEntries = contractSchema.metadata?.specEntries as xdr.ScSpecEntry[] | undefined;
if (specEntries && isEnumType(specEntries, param.type)) {
const enumMetadata = extractEnumVariants(specEntries, param.type);
const enumValue = valueToParse as EnumValue;
if (enumMetadata && enumValue.values) {
// Find the variant metadata for the selected tag
const selectedVariant = enumMetadata.variants.find((v) => v.name === enumValue.tag);
if (selectedVariant && selectedVariant.payloadTypes) {
// Process each payload value according to its expected type
const processedValues = enumValue.values.map(
(rawValue: unknown, payloadIndex: number) => {
const expectedType = selectedVariant.payloadTypes![payloadIndex];
if (expectedType) {
// Use parseStellarInput to convert the raw value to the proper type
const processedValue = parseStellarInput(rawValue, expectedType);
// Create SorobanArgumentValue structure for getScValFromArg
return {
type: expectedType,
value: processedValue,
};
}
return rawValue;
}
);
// Return the enum with processed payload values
valueToParse = { ...enumValue, values: processedValues };
}
}
}
}
// Handle array parameters - if the value is already an array and the type expects it,
// we can pass it directly. The Stellar input parser will handle the conversion.
if (
typeof param.type === 'string' &&
param.type.startsWith('Vec<') &&
Array.isArray(valueToParse)
) {
// For Vec types, pass array as-is to parseStellarInput
return parseStellarInput(valueToParse, param.type);
}
return parseStellarInput(valueToParse, param.type);
});
// --- Step 4: Validate Contract Address --- //
if (!contractSchema.address) {
throw new Error('Contract address is missing or invalid in the provided schema.');
}
try {
Address.fromString(contractSchema.address);
} catch {
throw new Error('Contract address is missing or invalid in the provided schema.');
}
// --- Step 5: Prepare Return Object --- //
const stellarTransactionData: StellarTransactionData = {
contractAddress: contractSchema.address,
functionName: functionDetails.name,
args: transformedArgs,
argTypes: functionDetails.inputs.map((param) => param.type), // Include parameter types for ScVal conversion
argSchema: functionDetails.inputs, // Include full parameter schema with struct field definitions
transactionOptions: {
// Add any Stellar-specific transaction options here
// For example: fee, timeout, memo, etc.
},
};
logger.debug(
'formatStellarTransactionData',
'Formatted transaction data:',
stellarTransactionData
);
return stellarTransactionData;
}