From 7a2b6443e958027a703e0c3aad3f62a21671b807 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 10 Apr 2024 12:08:41 +0200 Subject: [PATCH 01/11] handle empty function names --- src/plugins/oSnap/components/Input/MethodParameter.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/plugins/oSnap/components/Input/MethodParameter.vue b/src/plugins/oSnap/components/Input/MethodParameter.vue index c14260d2..e8406000 100644 --- a/src/plugins/oSnap/components/Input/MethodParameter.vue +++ b/src/plugins/oSnap/components/Input/MethodParameter.vue @@ -36,7 +36,10 @@ const inputType = computed(() => { return 'text'; }); -const label = `${props.parameter.name} (${props.parameter.type})`; +// function name may be null or empty string +const label = `${ + props.parameter.name?.length ? props.parameter.name + ' ' : '' +}(${props.parameter.type})`; const arrayPlaceholder = `E.g. ["text", 123, 0x123]`; const newValue = ref(props.value); From 06316c349c09e182e5644476adbbf2a8e2678185 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 10 Apr 2024 19:50:59 +0200 Subject: [PATCH 02/11] parse and validate array and tuples param inputs --- .../oSnap/components/Input/Address.vue | 1 + .../components/Input/MethodParameter.vue | 207 ++++++++++-------- src/plugins/oSnap/utils/validators.ts | 81 ++++++- 3 files changed, 202 insertions(+), 87 deletions(-) diff --git a/src/plugins/oSnap/components/Input/Address.vue b/src/plugins/oSnap/components/Input/Address.vue index eddd5636..02feec9f 100644 --- a/src/plugins/oSnap/components/Input/Address.vue +++ b/src/plugins/oSnap/components/Input/Address.vue @@ -62,6 +62,7 @@ const handleBlur = () => { 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; @@ -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(); + const errorMessageForDisplay = computed(() => { if (!isInputValid.value) { return validationErrorMessage.value @@ -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, () => { @@ -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 | 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; @@ -143,6 +176,7 @@ function formatBytes32() { newValue.value = hexZeroPad(newValue.value, 32); } } + onMounted(() => { if (props.validateOnMount) { isDirty.value = true; @@ -152,8 +186,17 @@ onMounted(() => { diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 1b5b75d0..c9e1829a 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -200,3 +200,7 @@ export function toChecksumAddress(address: string) { return address; } } + +export function addressEqual(address1: string, address2: string) { + return address1.toLowerCase() === address2.toLowerCase(); +} diff --git a/src/plugins/oSnap/Create.vue b/src/plugins/oSnap/Create.vue index f4cc6fcc..d4942df8 100644 --- a/src/plugins/oSnap/Create.vue +++ b/src/plugins/oSnap/Create.vue @@ -49,19 +49,16 @@ const collectables = ref([]); function addTransaction(transaction: Transaction) { if (newPluginData.value.safe === null) return; newPluginData.value.safe.transactions.push(transaction); - update(newPluginData.value); } function removeTransaction(transactionIndex: number) { if (!newPluginData.value.safe) return; newPluginData.value.safe.transactions.splice(transactionIndex, 1); - update(newPluginData.value); } function updateTransaction(transaction: Transaction, transactionIndex: number) { if (!newPluginData.value.safe) return; newPluginData.value.safe.transactions[transactionIndex] = transaction; - update(newPluginData.value); } async function fetchTokens(url: string): Promise { diff --git a/src/plugins/oSnap/components/Input/Address.vue b/src/plugins/oSnap/components/Input/Address.vue index 00895563..eddd5636 100644 --- a/src/plugins/oSnap/components/Input/Address.vue +++ b/src/plugins/oSnap/components/Input/Address.vue @@ -7,6 +7,7 @@ const props = defineProps<{ error?: string; disabled?: boolean; }>(); + const emit = defineEmits<{ 'update:modelValue': [value: string]; }>(); @@ -24,6 +25,7 @@ const validate = () => { error.value = 'Address is required'; return; } + if (!mustBeEthereumAddress(input.value)) { error.value = 'Invalid address'; return; @@ -49,6 +51,11 @@ onMounted(() => { const handleInput = () => { emit('update:modelValue', input.value); }; + +const handleBlur = () => { + dirty.value = true; + validate(); +}; diff --git a/src/plugins/oSnap/components/TransactionBuilder/RawTransaction.vue b/src/plugins/oSnap/components/TransactionBuilder/RawTransaction.vue index 357f8783..19d58d5f 100644 --- a/src/plugins/oSnap/components/TransactionBuilder/RawTransaction.vue +++ b/src/plugins/oSnap/components/TransactionBuilder/RawTransaction.vue @@ -1,7 +1,5 @@ + + diff --git a/src/plugins/oSnap/components/TransactionBuilder/Transaction.vue b/src/plugins/oSnap/components/TransactionBuilder/Transaction.vue index e5251c02..a6185607 100644 --- a/src/plugins/oSnap/components/TransactionBuilder/Transaction.vue +++ b/src/plugins/oSnap/components/TransactionBuilder/Transaction.vue @@ -1,5 +1,4 @@