Commit 51b23761 authored by tom's avatar tom

transform fields into args

parent 2334ef5b
import dotenv from 'dotenv';
import { TextEncoder, TextDecoder } from 'util';
import fetchMock from 'jest-fetch-mock';
......@@ -6,6 +7,8 @@ fetchMock.enableMocks();
const envs = dotenv.config({ path: './configs/envs/.env.jest' });
Object.assign(global, { TextDecoder, TextEncoder });
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
......
export default function bytesToHexString(bytes: Uint8Array) {
return Array.from(bytes, (byte) => {
return ('0' + (byte & 0xff).toString(16)).slice(-2);
}).join('');
}
import type { Abi } from 'abitype';
import type { Abi, AbiType } from 'abitype';
export type SmartContractMethodArgType = 'address' | 'uint256' | 'bool' | 'string' | 'bytes' | 'bytes32' | 'bytes32[]' | 'tuple';
export type SmartContractMethodArgType = AbiType;
export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable';
export interface SmartContract {
......
import { Box, Button, chakra, Flex, Grid, Icon, Text } from '@chakra-ui/react';
// import _fromPairs from 'lodash/fromPairs';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form';
......@@ -11,6 +10,7 @@ import arrowIcon from 'icons/arrows/down-right.svg';
import * as mixpanel from 'lib/mixpanel/index';
import ContractMethodCallableRow from './ContractMethodCallableRow';
import { formatFieldValues, transformFieldsToArgs } from './utils';
interface ResultComponentProps<T extends SmartContractMethod> {
item: T;
......@@ -25,52 +25,9 @@ interface Props<T extends SmartContractMethod> {
isWrite?: boolean;
}
// TODO @tom2drum remove this
const getFieldName = (name: string | undefined, index: number): string => name || String(index);
const getFormFieldName = (inputName: string, inputIndex: number, group?: string) => `${ group ? `${ group }_` : '' }${ inputName || 'input' }-${ inputIndex }}`;
const sortFields = (data: Array<SmartContractMethodInput>) => (
[ a ]: [string, string | Array<string>],
[ b ]: [string, string | Array<string>],
): 1 | -1 | 0 => {
const fieldNames = data.map(({ name }, index) => getFieldName(name, index));
const indexA = fieldNames.indexOf(a);
const indexB = fieldNames.indexOf(b);
if (indexA > indexB) {
return 1;
}
if (indexA < indexB) {
return -1;
}
return 0;
};
const castFieldValue = (data: Array<SmartContractMethodInput>) => ([ key, value ]: [ string, string | Array<string> ], index: number) => {
if (data[index].type.includes('[')) {
return [ key, parseArrayValue(value) ];
}
return [ key, value ];
};
const parseArrayValue = (value: string | Array<string>) => {
try {
if (Array.isArray(value)) {
return value;
}
const parsedResult = JSON.parse(value);
if (Array.isArray(parsedResult)) {
return parsedResult;
}
throw new Error('Not an array');
} catch (error) {
return '';
}
};
// groupName%groupIndex:inputName%inputIndex
const getFormFieldName = (input: { index: number; name: string }, group?: { index: number; name: string }) =>
`${ group ? `${ group.name }%${ group.index }:` : '' }${ input.name || 'input' }%${ input.index }`;
const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit, resultComponent: ResultComponent, isWrite }: Props<T>) => {
......@@ -89,8 +46,6 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
}, [ data ]);
const formApi = useForm<MethodFormFields>({
// TODO @tom2drum rewrite this
// defaultValues: _fromPairs(inputs.map(({ name }, index) => [ getFieldName(name, index), '' ])),
mode: 'onBlur',
});
......@@ -103,14 +58,8 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
}, [ result ]);
const onFormSubmit: SubmitHandler<MethodFormFields> = React.useCallback(async(formData) => {
// console.log('__>__ formData', formData);
// debugger;
const args = Object.entries(formData)
.sort(sortFields(inputs))
.map(castFieldValue(inputs))
.map(([ , value ]) => value);
const formattedData = formatFieldValues(formData, inputs);
const args = transformFieldsToArgs(formattedData);
setResult(undefined);
setLoading(true);
......@@ -147,7 +96,7 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
_empty={{ display: 'none' }}
>
{ inputs.map((input, index) => {
const fieldName = getFormFieldName(input.name, index);
const fieldName = getFormFieldName({ name: input.name, index });
if (input.type === 'tuple' && input.components) {
return (
......@@ -164,7 +113,10 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
</Box>
<div/>
{ input.components.map((component, componentIndex) => {
const fieldName = getFormFieldName(component.name, componentIndex, input.name);
const fieldName = getFormFieldName(
{ name: component.name, index: componentIndex },
{ name: input.name, index },
);
return (
<ContractMethodCallableRow
......
......@@ -3,6 +3,9 @@ import type { SmartContractQueryMethodRead, SmartContractMethod } from 'types/ap
import type { ResourceError } from 'lib/api/resources';
export type MethodFormFields = Record<string, string | Array<string>>;
export type MethodFormFieldsFormatted = Record<string, MethodArgType>;
export type MethodArgType = string | boolean | Array<MethodArgType>;
export type ContractMethodReadResult = SmartContractQueryMethodRead | ResourceError;
......
import { prepareAbi } from './utils';
import type { SmartContractMethodInput } from 'types/api/contract';
import { prepareAbi, transformFieldsToArgs, formatFieldValues } from './utils';
describe('function prepareAbi()', () => {
const commonAbi = [
......@@ -98,3 +100,100 @@ describe('function prepareAbi()', () => {
expect(item).toEqual(commonAbi[2]);
});
});
describe('function formatFieldValues()', () => {
const formFields = {
'_tx%0:nonce%0': '1 000 000 000 000 000 000',
'_tx%0:sender%1': '0xB375d4150A853482f25E3922A4C64c6C4fF6Ae3c',
'_tx%0:targets%2': [
'1',
'true',
],
'_l2OutputIndex%1': '1',
'_paused%2': '0',
'_withdrawalProof%3': [
'xxx',
'0x02',
],
};
const inputs: Array<SmartContractMethodInput> = [
{
components: [
{ internalType: 'uint256', name: 'nonce', type: 'uint256' },
{ internalType: 'address', name: 'sender', type: 'address' },
{ internalType: 'bool[]', name: 'targets', type: 'bool[]' },
],
internalType: 'tuple',
name: '_tx',
type: 'tuple',
},
{ internalType: 'bytes32', name: '_l2OutputIndex', type: 'bytes32' },
{
internalType: 'bool',
name: '_paused',
type: 'bool',
},
{
internalType: 'bytes32[]',
name: '_withdrawalProof',
type: 'bytes32[]',
},
];
it('converts values to correct format', () => {
const result = formatFieldValues(formFields, inputs);
expect(result).toEqual({
'_tx%0:nonce%0': '1000000000000000000',
'_tx%0:sender%1': '0xB375d4150A853482f25E3922A4C64c6C4fF6Ae3c',
'_tx%0:targets%2': [
true,
true,
],
'_l2OutputIndex%1': '0x31',
'_paused%2': false,
'_withdrawalProof%3': [
'0x787878',
'0x02',
],
});
});
it('converts nested array string representation to correct format', () => {
const formFields = {
'_withdrawalProof%0': '[ [ 1 ], [ 2, 3 ], [ 4 ]]',
};
const inputs: Array<SmartContractMethodInput> = [
{ internalType: 'uint[][]', name: '_withdrawalProof', type: 'uint[][]' },
];
const result = formatFieldValues(formFields, inputs);
expect(result).toEqual({
'_withdrawalProof%0': [ [ 1 ], [ 2, 3 ], [ 4 ] ],
});
});
});
describe('function transformFieldsToArgs()', () => {
it('groups struct and array fields', () => {
const formFields = {
'_paused%2': 'primitive_1',
'_l2OutputIndex%1': 'primitive_0',
'_tx%0:nonce%0': 'struct_0',
'_tx%0:sender%1': 'struct_1',
'_tx%0:target%2': [ 'struct_2_0', 'struct_2_1' ],
'_withdrawalProof%3': [
'array_0',
'array_1',
],
};
const args = transformFieldsToArgs(formFields);
expect(args).toEqual([
[ 'struct_0', 'struct_1', [ 'struct_2_0', 'struct_2_1' ] ],
'primitive_0',
'primitive_1',
[ 'array_0', 'array_1' ],
]);
});
});
import type { Abi } from 'abitype';
import _mapValues from 'lodash/mapValues';
import type { SmartContractWriteMethod } from 'types/api/contract';
import type { MethodArgType, MethodFormFields, MethodFormFieldsFormatted } from './types';
import type { SmartContractMethodArgType, SmartContractMethodInput, SmartContractWriteMethod } from 'types/api/contract';
import bytesToHexString from 'lib/bytesToHexString';
import stringToBytes from 'lib/stringToBytes';
export const INT_REGEXP = /^(u)?int(\d+)?$/i;
......@@ -89,3 +94,116 @@ export function prepareAbi(abi: Abi, item: SmartContractWriteMethod): Abi {
return abi;
}
function getFieldType(fieldName: string, inputs: Array<SmartContractMethodInput>) {
const chunks = fieldName.split(':');
if (chunks.length === 1) {
const [ , index ] = chunks[0].split('%');
return inputs[Number(index)].type;
} else {
const group = chunks[0].split('%');
const input = chunks[1].split('%');
return inputs[Number(group[1])].components?.[Number(input[1])].type;
}
}
function parseArrayValue(value: string) {
try {
const parsedResult = JSON.parse(value);
if (Array.isArray(parsedResult)) {
return parsedResult as Array<string>;
}
throw new Error('Not an array');
} catch (error) {
return '';
}
}
function castValue(value: string, type: SmartContractMethodArgType) {
if (type === 'bool') {
return formatBooleanValue(value) === 'true';
}
const intMatch = type.match(INT_REGEXP);
if (intMatch) {
return value.replaceAll(' ', '');
}
const bytesMatch = type.match(BYTES_REGEXP);
if (bytesMatch) {
if (value.startsWith('0x')) {
return value;
}
const bytesArray = stringToBytes(value);
return `0x${ bytesToHexString(bytesArray) }`;
}
const isNestedArray = (type.match(/\[/g) || []).length > 1;
if (isNestedArray) {
return parseArrayValue(value) || value;
}
return value;
}
export function formatFieldValues(formFields: MethodFormFields, inputs: Array<SmartContractMethodInput>) {
const formattedFields = _mapValues(formFields, (value, key) => {
const type = getFieldType(key, inputs);
if (!type) {
return value;
}
if (Array.isArray(value)) {
const arrayMatch = type.match(ARRAY_REGEXP);
if (arrayMatch) {
return value.map((item) => castValue(item, arrayMatch[1] as SmartContractMethodArgType));
}
return value;
}
return castValue(value, type);
});
return formattedFields;
}
export function transformFieldsToArgs(formFields: MethodFormFieldsFormatted) {
const unGroupedFields = Object.entries(formFields)
.reduce((
result: Record<string, MethodArgType>,
[ key, value ]: [ string, MethodArgType ],
) => {
const chunks = key.split(':');
if (chunks.length > 1) {
const groupKey = chunks[0];
const [ , fieldIndex ] = chunks[1].split('%');
if (result[groupKey] === undefined) {
result[groupKey] = [];
}
(result[groupKey] as Array<MethodArgType>)[Number(fieldIndex)] = value;
return result;
}
result[key] = value;
return result;
}, {});
const args = (Object.entries(unGroupedFields)
.map(([ key, value ]) => {
const [ , index ] = key.split('%');
return [ Number(index), value ];
}) as Array<[ number, string | Array<string> ]>)
.sort((a, b) => a[0] - b[0])
.map(([ , value ]) => value);
return args;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment