Skip to content

Commit

Permalink
parse and validate array and tuples param inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
gsteenkamp89 committed Apr 10, 2024
1 parent 7a2b644 commit 06316c3
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 87 deletions.
1 change: 1 addition & 0 deletions src/plugins/oSnap/components/Input/Address.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const handleBlur = () => {
<UiInput
v-model="input"
:disabled="disabled"
placeholder="0x123...abc"
:error="props.error ?? (error || '')"
@input="handleInput()"
@blur="handleBlur"
Expand Down
207 changes: 121 additions & 86 deletions src/plugins/oSnap/components/Input/MethodParameter.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<script setup lang="ts">
import { ParamType } from '@ethersproject/abi';
import { isAddress } from '@ethersproject/address';
import { isBigNumberish } from '@ethersproject/bignumber/lib/bignumber';
import AddressInput from './Address.vue';
import { hexZeroPad, isBytesLike } from '@ethersproject/bytes';
import { hexZeroPad } from '@ethersproject/bytes';
import {
InputTypes,
validateArrayInput,
validateInput,
validateTupleInput
} from '../../utils';
const props = defineProps<{
parameter: ParamType;
Expand All @@ -16,37 +20,95 @@ const emit = defineEmits<{
}>();
const isDirty = ref(false);
const isBooleanInput = computed(() => props.parameter.baseType === 'bool');
const isAddressInput = computed(() => props.parameter.baseType === 'address');
const isNumberInput = computed(() => props.parameter.baseType.includes('int'));
const isBytesInput = computed(() => props.parameter.baseType === 'bytes');
const isBytes32Input = computed(() => props.parameter.baseType === 'bytes32');
const isArrayInput = computed(
() =>
props.parameter.baseType === 'array' || props.parameter.baseType === 'tuple'
);
const placeholders = {
string: 'a string of text',
address: '0x123...abc',
int: '123456',
bytes: '0x123abc',
bytes32: '0x123abc',
bool: 'true'
} as const;
function reduceInt(type: string) {
if (type.includes('int')) {
return 'int';
}
return type;
}
const inputType = computed(() => {
if (isBooleanInput.value) return 'boolean';
if (isAddressInput.value) return 'address';
if (isNumberInput.value) return 'number';
if (isBytesInput.value) return 'bytes';
if (isBytes32Input.value) return 'bytes32';
if (isArrayInput.value) return 'array';
return 'text';
const baseType = props.parameter.baseType;
if (baseType === 'array') {
if (props.parameter.type.includes('tuple')) {
return {
input: 'tuple',
type: props.parameter.components.map(
item => reduceInt(item.baseType) as InputTypes
)
// ["string","int","address"]
} as const;
} else {
return {
input: 'array',
type: reduceInt(props.parameter.arrayChildren.baseType) as InputTypes
} as const;
}
}
return { type: reduceInt(baseType) as InputTypes, input: 'single' } as const;
});
// function name may be null or empty string
const label = `${
const isBooleanInput = computed(
() => inputType.value.input === 'single' && inputType.value.type === 'bool'
);
const isStringInput = computed(
() => inputType.value.input === 'single' && inputType.value.type === 'string'
);
const isAddressInput = computed(
() => inputType.value.input === 'single' && inputType.value.type === 'address'
);
const isNumberInput = computed(
() => inputType.value.input === 'single' && inputType.value.type === 'int'
);
const isBytesInput = computed(
() => inputType.value.input === 'single' && inputType.value.type === 'bytes'
);
const isBytes32Input = computed(
() => inputType.value.input === 'single' && inputType.value.type === 'bytes32'
);
const isArrayInput = computed(() => inputType.value.input !== 'single');
// param name may be null or empty string
const paramName = `${
props.parameter.name?.length ? props.parameter.name + ' ' : ''
}(${props.parameter.type})`;
const arrayPlaceholder = `E.g. ["text", 123, 0x123]`;
}`;
const paramType = computed(() => {
if (inputType.value.input === 'single') {
return `(${inputType.value.type})`;
}
return `( ${inputType.value.type}[ ] )`;
});
const label = paramName + paramType.value;
const arrayPlaceholder = computed(() => {
if (inputType.value.input === 'array') {
return `E.g. [${placeholders[inputType.value.type]}]`;
}
if (inputType.value.input === 'tuple') {
return `E.g. [${inputType.value.type.map(type => placeholders[type])}]`;
}
});
const newValue = ref(props.value);
const validationState = ref(true);
const isInputValid = computed(() => validationState.value);
const validationErrorMessage = ref<string>();
const errorMessageForDisplay = computed(() => {
if (!isInputValid.value) {
return validationErrorMessage.value
Expand All @@ -64,12 +126,13 @@ const allowQuickFixForBytes32 = computed(() => {
function validate() {
if (!isDirty.value) return true;
if (isAddressInput.value) return isAddress(newValue.value);
if (isArrayInput.value) return validateArrayInput(newValue.value);
if (isNumberInput.value) return validateNumberInput(newValue.value);
if (isBytes32Input.value) return validateBytes32Input(newValue.value);
if (isBytesInput.value) return validateBytesInput(newValue.value);
return true;
if (inputType.value.input === 'array') {
return validateArrayInput(newValue.value, inputType.value.type);
}
if (inputType.value.input === 'tuple') {
return validateTupleInput(newValue.value, inputType.value.type);
}
return validateInput(newValue.value, inputType.value.type);
}
watch(props.parameter, () => {
Expand All @@ -86,52 +149,22 @@ watch(newValue, () => {
emit('updateParameterValue', newValue.value);
});
function validateNumberInput(value: string) {
return isBigNumberish(value);
}
function validateBytesInput(value: string) {
return isBytesLike(value);
}
// provide better feedback/validation messages for bytes32 inputs
function validateBytes32Input(value: string) {
try {
watch(newValue, value => {
if (isBytes32Input.value && !isArrayInput.value) {
const data = value?.slice(2) || '';
if (data.length < 64) {
validationErrorMessage.value = 'Value too short';
throw new Error('Less than 32 bytes');
validationErrorMessage.value = 'bytes32 too short';
return;
}
if (data.length > 64) {
validationErrorMessage.value = 'Value too long';
throw new Error('More than 32 bytes');
}
if (!isBytesLike(value)) {
throw new Error('Invalid bytes32');
validationErrorMessage.value = 'bytes32 too long';
return;
}
return true;
} catch {
return false;
}
}
function validateArrayInput(value: string) {
try {
const parsedValue = JSON.parse(value) as Array<string> | unknown;
if (!Array.isArray(parsedValue)) return false;
if (
props.parameter.arrayLength !== -1 &&
parsedValue.length !== props.parameter.arrayLength
)
return false;
return true;
} catch (e) {
return false;
validationErrorMessage.value = '';
}
}
});
function onChange(value: string) {
newValue.value = value;
Expand All @@ -143,6 +176,7 @@ function formatBytes32() {
newValue.value = hexZeroPad(newValue.value, 32);
}
}
onMounted(() => {
if (props.validateOnMount) {
isDirty.value = true;
Expand All @@ -152,8 +186,17 @@ onMounted(() => {
</script>

<template>
<UiInput
v-if="isArrayInput"
:placeholder="arrayPlaceholder"
:error="errorMessageForDisplay"
:model-value="value"
@update:model-value="onChange($event)"
>
<template #label>{{ label }}</template>
</UiInput>
<UiSelect
v-if="inputType === 'boolean'"
v-if="isBooleanInput"
:model-value="value"
@update:model-value="onChange($event)"
>
Expand All @@ -163,41 +206,33 @@ onMounted(() => {
</UiSelect>

<AddressInput
v-if="inputType === 'address'"
v-if="isAddressInput"
:label="label"
:model-value="value"
@update:model-value="onChange($event)"
/>

<UiInput
v-if="inputType === 'array'"
:placeholder="arrayPlaceholder"
:error="errorMessageForDisplay"
:model-value="value"
@update:model-value="onChange($event)"
>
<template #label>{{ label }}</template>
</UiInput>
<UiInput
v-if="inputType === 'number'"
placeholder="123456"
v-if="isNumberInput"
:placeholder="placeholders.int"
:error="errorMessageForDisplay"
:model-value="value"
@update:model-value="onChange($event)"
>
<template #label>{{ label }}</template>
</UiInput>
<UiInput
v-if="inputType === 'bytes'"
placeholder="0x123abc"
v-if="isBytesInput"
:placeholder="placeholders.bytes"
:error="errorMessageForDisplay"
:model-value="value"
@update:model-value="onChange($event)"
>
<template #label>{{ label }}</template>
</UiInput>
<UiInput
v-if="inputType === 'bytes32'"
placeholder="0x123abc"
v-if="isBytes32Input"
:placeholder="placeholders.bytes"
:error="errorMessageForDisplay"
:model-value="value"
:quick-fix="allowQuickFixForBytes32 ? formatBytes32 : undefined"
Expand All @@ -207,8 +242,8 @@ onMounted(() => {
<template #label>{{ label }}</template>
</UiInput>
<UiInput
v-if="inputType === 'text'"
placeholder="a string of text"
v-if="isStringInput"
:placeholder="placeholders.string"
:model-value="value"
@update:model-value="onChange($event)"
>
Expand Down
Loading

0 comments on commit 06316c3

Please sign in to comment.