From 2b509cc6838b44ebb770cdcb3fb948f63f94503d Mon Sep 17 00:00:00 2001 From: Viterbo Date: Tue, 12 Nov 2024 14:37:33 -0300 Subject: [PATCH 1/4] new user friendly component TupleStruct --- .../ContractTab/FunctionInterface.vue | 153 +++++----- src/components/inputs/TupleStruct.vue | 267 ++++++++++++++++++ src/lib/function-interface-utils-ts.ts | 82 ++++++ src/lib/function-interface-utils.js | 2 +- src/types/FunctionInterfaces.ts | 14 + src/types/index.ts | 2 + 6 files changed, 433 insertions(+), 87 deletions(-) create mode 100644 src/components/inputs/TupleStruct.vue create mode 100644 src/lib/function-interface-utils-ts.ts create mode 100644 src/types/FunctionInterfaces.ts diff --git a/src/components/ContractTab/FunctionInterface.vue b/src/components/ContractTab/FunctionInterface.vue index ff210301..cbbf650d 100644 --- a/src/components/ContractTab/FunctionInterface.vue +++ b/src/components/ContractTab/FunctionInterface.vue @@ -13,22 +13,17 @@ import { EvmABI, EvmFunctionParam } from 'src/antelope/types'; import { WEI_PRECISION } from 'src/antelope/wallets/utils'; import { asyncInputComponents, - getComponentForInputType, - getExpectedArrayLengthFromParameterType, - getIntegerBits, - inputIsComplex, - parameterIsArrayType, - parameterIsIntegerType, - parameterTypeIsBoolean, - parameterTypeIsSignedIntArray, - parameterTypeIsTupleStruct, - parameterTypeIsTupleStructArray, - parameterTypeIsUnsignedIntArray, } from 'src/lib/function-interface-utils'; import TransactionField from 'src/components/TransactionField.vue'; import LoginModal from 'components/LoginModal.vue'; import FunctionOutputViewer from 'components/ContractTab/FunctionOutputViewer.vue'; -import { OutputType, OutputValue, InputDescription } from 'src/types'; +import { + InputComponent, + OutputType, + OutputValue, + inputComponents, +} from 'src/types'; +import { getComponentsForAbiInputs } from 'src/lib/function-interface-utils-ts'; interface Opts { value?: string; @@ -97,8 +92,12 @@ export default defineComponent({ selectDecimals: decimalOptions[0], customDecimals: 0, // number value: '0', // string - inputModels: [] as string[], // raw input values - params: [] as EvmFunctionParam[], // parsed input values + missingInputs: true, // boolean + models: { + inputs: [] as string[], // raw input values + values: [] as EvmFunctionParam[], // parsed input values + }, + valueParam: { 'name': 'value', 'type': 'amount', @@ -121,87 +120,69 @@ export default defineComponent({ functionABI(){ return `${this.abi.name}(${this.abi.inputs.map((i: { type: never; }) => i.type).join(',')})`; }, - inputComponents() { - if (!Array.isArray(this.abi?.inputs)) { - return []; + inputComponents(): inputComponents { + const components = getComponentsForAbiInputs(this.abi?.inputs, this.models) as unknown as inputComponents; + return components; + }, + /* missingInputs() { // FIXME: remove + console.log('FunctionInterface.missingInputs() ...'); + const inputs_length = this.abi.inputs.length; + const values_length = this.models.values.length; + if (inputs_length !== values_length) { + console.log('FunctionInterface.missingInputs()', { inputs_length, values_length }); // FIXME: remove + return true; } - const getExtraBindingsForType = (input: InputDescription, index: number) => { - const { type, name, components } = input; - const label = `${name ? name : `Param ${index + 1}`}`; - const extras = {} as {[key:string]: string | InputDescription[]}; - - // represents integer bits (e.g. uint256) for int types, or array length for array types - let size = undefined; - if (parameterIsArrayType(type)) { - size = getExpectedArrayLengthFromParameterType(type); - } else if (parameterIsIntegerType(type)) { - size = getIntegerBits(type); - } else if (parameterTypeIsTupleStruct(type) && components) { - size = toRaw(components).length; - } + if (inputs_length !== values_length) { + return true; + } - const result = type.match(/(\d+)(?=\[)/); - const intSize = result ? result[0] : undefined; - - if (intSize && parameterTypeIsUnsignedIntArray(type)) { - extras['uint-size'] = intSize; - } else if (intSize && parameterTypeIsSignedIntArray(type)) { - extras['int-size'] = intSize; - } else if (parameterTypeIsTupleStruct(type) && components) { - extras['componentDescription'] = toRaw(components); - } else if (parameterTypeIsTupleStructArray(type) && components) { - extras['componentDescription'] = toRaw(components); + for (let i = 0; i < this.abi.inputs.length; i++) { + if (['', null, undefined].includes(this.models.values[i] as never)) { + console.log('FunctionInterface.missingInputs()', { i, value: this.models.values[i] }); // FIXME: remove + return true; } + } - const defaultModelValue = parameterTypeIsBoolean(type) ? null : ''; - - const bindings = { - ...extras, - label, - size, - modelValue: this.inputModels[index] ?? defaultModelValue, - name: label.toLowerCase(), - }; - return bindings; - }; - - const handleModelValueChange = (type: string, index: number, value: string) => { - this.inputModels[index] = value; - - if (!inputIsComplex(type)) { - this.params[index] = value; - } - }; - const handleValueParsed = (type: string, index: number, value: EvmFunctionParam) => { - if (inputIsComplex(type)) { - this.params[index] = value; - } - }; - - return this.abi.inputs.map((input, index) => ({ - bindings: getExtraBindingsForType(input, index), - is: getComponentForInputType(input.type), - inputType: input.type, - handleModelValueChange: (type: string, index: number, value: string) => handleModelValueChange(type, index, value), - handleValueParsed: (type: string, index: number, value: EvmFunctionParam) => handleValueParsed(type, index, value), - })); + console.log('FunctionInterface.missingInputs()', { inputs_length, values_length }); // FIXME: remove + return false; + }, + */ + }, + methods: { + valueParsed(inputType: string, index: number, value: EvmFunctionParam, component: InputComponent) { + console.log('FunctionInterface.valueParsed()', inputType, index, value); // FIXME: remove + component.handleValueParsed(inputType, index, value); + this.updateMissingInputs(); }, - missingInputs() { - if (this.abi.inputs.length !== this.params.length) { + updateMissingInputs() { + console.log('FunctionInterface.updateMissingInputs() ...'); + const inputs_length = this.abi.inputs.length; + const values_length = this.models.values.length; + if (inputs_length !== values_length) { + console.log('FunctionInterface.updateMissingInputs()', { inputs_length, values_length }); // FIXME: remove + this.missingInputs = true; + return true; + } + + if (inputs_length !== values_length) { + this.missingInputs = true; return true; } for (let i = 0; i < this.abi.inputs.length; i++) { - if (['', null, undefined].includes(this.params[i] as never)) { + if (['', null, undefined].includes(this.models.values[i] as never)) { + console.log('FunctionInterface.updateMissingInputs()', { i, value: this.models.values[i] }); // FIXME: remove + this.missingInputs = true; return true; } } + const values = this.models.values; + console.log('FunctionInterface.updateMissingInputs()', { inputs_length, values_length, values }); // FIXME: remove + this.missingInputs = false; return false; }, - }, - methods: { showAmountDialog(param: string) { this.amountParam = param; this.amountDecimals = 18; @@ -217,7 +198,7 @@ export default defineComponent({ if (typeof this.amountParam === 'string') { this.value = integerAmount; } else { - this.params[this.amountParam] = integerAmount as never; + this.models.values[this.amountParam] = integerAmount as never; } this.clearAmount(); @@ -272,7 +253,7 @@ export default defineComponent({ }, runRead() { return this.getEthersFunction() - .then(func => func(...this.params) + .then(func => func(...this.models.values) .then((response: OutputValue | OutputValue[]) => { this.result = response as unknown as string; this.response = Array.isArray(response) ? response : [response]; @@ -289,8 +270,8 @@ export default defineComponent({ const contractInstance = toRaw(await this.contract.getContractInstance()); const func = contractInstance.populateTransaction[this.functionABI]; const gasEstimater = contractInstance.estimateGas[this.functionABI]; - const gasLimit = await gasEstimater(...this.params, Object.assign({ from: this.address }, opts)); - const unsignedTrx = await func(...this.params, opts); + const gasLimit = await gasEstimater(...this.models.values, Object.assign({ from: this.address }, opts)); + const unsignedTrx = await func(...this.models.values, opts); const nonce = parseInt(await this.$evm.telos.getNonce(this.address), 16); const gasPrice = BigNumber.from(`0x${await this.$evm.telos.getGasPrice()}`); unsignedTrx.nonce = nonce; @@ -366,7 +347,7 @@ export default defineComponent({ error, this.contract.address, [this.abi] as EvmABI, - this.params, + this.models.values, value, ).then((result) => { this.hash = result.hash; @@ -449,8 +430,8 @@ export default defineComponent({ :key="index" v-bind="component.bindings" required="true" - class="input-component q-pb-lg" - @valueParsed="component.handleValueParsed(component.inputType, index, $event)" + class="input-component q-pb-md" + @valueParsed="valueParsed(component.inputType, index, $event, component)" @update:modelValue="component.handleModelValueChange(component.inputType, index, $event)" /> diff --git a/src/components/inputs/TupleStruct.vue b/src/components/inputs/TupleStruct.vue new file mode 100644 index 00000000..24289511 --- /dev/null +++ b/src/components/inputs/TupleStruct.vue @@ -0,0 +1,267 @@ + + + + + diff --git a/src/lib/function-interface-utils-ts.ts b/src/lib/function-interface-utils-ts.ts new file mode 100644 index 00000000..e6f065db --- /dev/null +++ b/src/lib/function-interface-utils-ts.ts @@ -0,0 +1,82 @@ +import { InputComponent, inputComponents, InputDescription } from 'src/types'; +import { getComponentForInputType, getExpectedArrayLengthFromParameterType, getIntegerBits, inputIsComplex, parameterIsArrayType, parameterIsIntegerType, parameterTypeIsBoolean, parameterTypeIsSignedIntArray, parameterTypeIsTupleStruct, parameterTypeIsTupleStructArray, parameterTypeIsUnsignedIntArray } from 'src/lib/function-interface-utils'; +import { toRaw } from 'vue'; +import { EvmFunctionParam } from 'src/antelope/types'; + +export function getComponentsForAbiInputs(inputs: InputDescription[], models: {inputs: string[], values: EvmFunctionParam[]}): inputComponents { + // inputs must be an array + if (!Array.isArray(inputs)) { + return []; + } + // models must be an object with a an array of values for each input (user input text, always strings) + if ( + typeof models !== 'object' || + !Array.isArray(models.values) + ) { + return []; + } + + const getExtraBindingsForType = (input: InputDescription, index: number) => { + const { type, name, components } = input; + const label = `${name ? name : `Param ${index + 1}`}`; + const extras = {} as {[key:string]: string | InputDescription[]}; + + // represents integer bits (e.g. uint256) for int types, or array length for array types + let size = undefined; + if (parameterIsArrayType(type)) { + size = getExpectedArrayLengthFromParameterType(type); + } else if (parameterIsIntegerType(type)) { + size = getIntegerBits(type); + } else if (parameterTypeIsTupleStruct(type) && components) { + size = toRaw(components).length; + } + + const result = type.match(/(\d+)(?=\[)/); + const intSize = result ? result[0] : undefined; + + if (intSize && parameterTypeIsUnsignedIntArray(type)) { + extras['uint-size'] = intSize; + } else if (intSize && parameterTypeIsSignedIntArray(type)) { + extras['int-size'] = intSize; + } else if (parameterTypeIsTupleStruct(type) && components) { + extras['componentDescription'] = toRaw(components); + } else if (parameterTypeIsTupleStructArray(type) && components) { + extras['componentDescription'] = toRaw(components); + } + + const defaultModelValue = parameterTypeIsBoolean(type) ? null : ''; + + const bindings = { + ...extras, + label, + size, + modelValue: models.inputs[index] ?? defaultModelValue, + name: label.toLowerCase(), + }; + return bindings; + }; + + const handleModelValueChange = (type: string, index: number, value: string) => { + models.inputs[index] = value; + + if (!inputIsComplex(type)) { + models.values[index] = value; + } + }; + const handleValueParsed = (type: string, index: number, value: EvmFunctionParam) => { + if (inputIsComplex(type)) { + models.values[index] = value; + } + }; + + return inputs.map((input, index) => ({ + bindings: getExtraBindingsForType(input, index), + is: getComponentForInputType(input.type), + inputType: input.type, + handleModelValueChange: (type: string, index: number, value: string) => handleModelValueChange(type, index, value), + handleValueParsed: (type: string, index: number, value: EvmFunctionParam) => handleValueParsed(type, index, value), + }) as unknown as InputComponent); + +} + + diff --git a/src/lib/function-interface-utils.js b/src/lib/function-interface-utils.js index c14e8e71..11613c9e 100644 --- a/src/lib/function-interface-utils.js +++ b/src/lib/function-interface-utils.js @@ -13,7 +13,7 @@ const asyncInputComponents = { UnsignedIntArrayInput: defineAsyncComponent(() => import('components/inputs/UnsignedIntArrayInput')), UnsignedIntInput: defineAsyncComponent(() => import('components/inputs/UnsignedIntInput')), SignedIntArrayInput: defineAsyncComponent(() => import('components/inputs/SignedIntArrayInput')), - TupleStructInput: defineAsyncComponent(() => import('components/inputs/TupleStructInput')), + TupleStructInput: defineAsyncComponent(() => import('components/inputs/TupleStruct')), TupleStructArrayInput: defineAsyncComponent(() => import('components/inputs/TupleStructArrayInput')), }; diff --git a/src/types/FunctionInterfaces.ts b/src/types/FunctionInterfaces.ts new file mode 100644 index 00000000..91bd6ca5 --- /dev/null +++ b/src/types/FunctionInterfaces.ts @@ -0,0 +1,14 @@ +import { EvmFunctionParam } from 'src/antelope/types'; +import { InputDescription } from 'src/types/AbiFunction'; + +export interface InputComponent { + bindings: { + [key: string]: string | InputDescription[]; + }; + is: string; + inputType: string; + handleModelValueChange: (type: string, index: number, value: string) => void; + handleValueParsed: (type: string, index: number, value: EvmFunctionParam) => void; +} + +export type inputComponents = InputComponent[]; diff --git a/src/types/index.ts b/src/types/index.ts index d9d2151b..30d85ff4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -8,3 +8,5 @@ export * from 'src/types/SearchTypes'; export * from 'src/types/TransactionQueryData'; export * from 'src/types/AbiFunction'; export * from 'src/types/Token'; +export * from 'src/types/FunctionInterfaces'; + From bbfe4201934956acae69da57711a97daffd148b0 Mon Sep 17 00:00:00 2001 From: Viterbo Date: Tue, 19 Nov 2024 13:26:04 -0300 Subject: [PATCH 2/4] better styling on tuple component input --- .../ContractTab/FunctionInterface.vue | 5 +- src/components/inputs/TupleStruct.vue | 63 +++++++++---------- src/lib/function-interface-utils-ts.ts | 3 +- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/src/components/ContractTab/FunctionInterface.vue b/src/components/ContractTab/FunctionInterface.vue index cbbf650d..92f1b4aa 100644 --- a/src/components/ContractTab/FunctionInterface.vue +++ b/src/components/ContractTab/FunctionInterface.vue @@ -121,7 +121,8 @@ export default defineComponent({ return `${this.abi.name}(${this.abi.inputs.map((i: { type: never; }) => i.type).join(',')})`; }, inputComponents(): inputComponents { - const components = getComponentsForAbiInputs(this.abi?.inputs, this.models) as unknown as inputComponents; + const isRoot = true; + const components = getComponentsForAbiInputs(this.abi?.inputs, this.models, isRoot) as unknown as inputComponents; return components; }, /* missingInputs() { // FIXME: remove @@ -463,7 +464,7 @@ export default defineComponent({ - diff --git a/src/lib/function-interface-utils-ts.ts b/src/lib/function-interface-utils-ts.ts index e6f065db..3c8af8b5 100644 --- a/src/lib/function-interface-utils-ts.ts +++ b/src/lib/function-interface-utils-ts.ts @@ -3,7 +3,7 @@ import { getComponentForInputType, getExpectedArrayLengthFromParameterType, getI import { toRaw } from 'vue'; import { EvmFunctionParam } from 'src/antelope/types'; -export function getComponentsForAbiInputs(inputs: InputDescription[], models: {inputs: string[], values: EvmFunctionParam[]}): inputComponents { +export function getComponentsForAbiInputs(inputs: InputDescription[], models: {inputs: string[], values: EvmFunctionParam[]}, isRoot: boolean): inputComponents { // inputs must be an array if (!Array.isArray(inputs)) { return []; @@ -48,6 +48,7 @@ export function getComponentsForAbiInputs(inputs: InputDescription[], models: {i const bindings = { ...extras, + isRoot, label, size, modelValue: models.inputs[index] ?? defaultModelValue, From a0caf2815491428746817d7e9b978ddd8dbad541 Mon Sep 17 00:00:00 2001 From: Viterbo Date: Wed, 20 Nov 2024 15:53:41 -0300 Subject: [PATCH 3/4] addressing PR comments --- .../ContractTab/FunctionInterface.vue | 30 ------------------- src/components/inputs/TupleStruct.vue | 8 +++-- 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/src/components/ContractTab/FunctionInterface.vue b/src/components/ContractTab/FunctionInterface.vue index 92f1b4aa..b5f43998 100644 --- a/src/components/ContractTab/FunctionInterface.vue +++ b/src/components/ContractTab/FunctionInterface.vue @@ -125,43 +125,16 @@ export default defineComponent({ const components = getComponentsForAbiInputs(this.abi?.inputs, this.models, isRoot) as unknown as inputComponents; return components; }, - /* missingInputs() { // FIXME: remove - console.log('FunctionInterface.missingInputs() ...'); - const inputs_length = this.abi.inputs.length; - const values_length = this.models.values.length; - if (inputs_length !== values_length) { - console.log('FunctionInterface.missingInputs()', { inputs_length, values_length }); // FIXME: remove - return true; - } - - if (inputs_length !== values_length) { - return true; - } - - for (let i = 0; i < this.abi.inputs.length; i++) { - if (['', null, undefined].includes(this.models.values[i] as never)) { - console.log('FunctionInterface.missingInputs()', { i, value: this.models.values[i] }); // FIXME: remove - return true; - } - } - - console.log('FunctionInterface.missingInputs()', { inputs_length, values_length }); // FIXME: remove - return false; - }, - */ }, methods: { valueParsed(inputType: string, index: number, value: EvmFunctionParam, component: InputComponent) { - console.log('FunctionInterface.valueParsed()', inputType, index, value); // FIXME: remove component.handleValueParsed(inputType, index, value); this.updateMissingInputs(); }, updateMissingInputs() { - console.log('FunctionInterface.updateMissingInputs() ...'); const inputs_length = this.abi.inputs.length; const values_length = this.models.values.length; if (inputs_length !== values_length) { - console.log('FunctionInterface.updateMissingInputs()', { inputs_length, values_length }); // FIXME: remove this.missingInputs = true; return true; } @@ -173,14 +146,12 @@ export default defineComponent({ for (let i = 0; i < this.abi.inputs.length; i++) { if (['', null, undefined].includes(this.models.values[i] as never)) { - console.log('FunctionInterface.updateMissingInputs()', { i, value: this.models.values[i] }); // FIXME: remove this.missingInputs = true; return true; } } const values = this.models.values; - console.log('FunctionInterface.updateMissingInputs()', { inputs_length, values_length, values }); // FIXME: remove this.missingInputs = false; return false; }, @@ -211,7 +182,6 @@ export default defineComponent({ this.showLoginModal = true; }, async run() { - console.log('run'); if (!this.isLoggedIn && this.write){ this.login(); return; diff --git a/src/components/inputs/TupleStruct.vue b/src/components/inputs/TupleStruct.vue index a301dd56..a16aca80 100644 --- a/src/components/inputs/TupleStruct.vue +++ b/src/components/inputs/TupleStruct.vue @@ -45,7 +45,6 @@ export default { fields: [], isVisible: false, animationTimer: 0, - // eventEmittingTimer: 0, // FIXME: remove models: { inputs: []/* as string[]*/, // raw input values values: []/* as EvmFunctionParam[]*/, // parsed input values @@ -214,7 +213,7 @@ export default { :key="index" v-bind="component.bindings" required="true" - class="input-component q-pb-lg" + class="tuple-struct__sub-component input-component q-pb-lg" :class="{ 'last-element': index === inputComponents.length - 1 }" @valueParsed="valueParsed(component.inputType, index, $event, component)" @update:modelValue="handleFieldChange(component.inputType, index, $event, component, inputComponents)" @@ -256,5 +255,10 @@ export default { transition: margin-right 1.3s; } + &__sub-component { + margin-left: 0px !important; + margin-right: 0px !important; + } + } From f2fcb1d468d1ceeb939f70875ed957f6cc2fc6e6 Mon Sep 17 00:00:00 2001 From: Viterbo Date: Wed, 20 Nov 2024 15:56:42 -0300 Subject: [PATCH 4/4] addressing PR comments (2) --- src/components/ContractTab/FunctionInterface.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/ContractTab/FunctionInterface.vue b/src/components/ContractTab/FunctionInterface.vue index b5f43998..90b8d000 100644 --- a/src/components/ContractTab/FunctionInterface.vue +++ b/src/components/ContractTab/FunctionInterface.vue @@ -150,8 +150,6 @@ export default defineComponent({ return true; } } - - const values = this.models.values; this.missingInputs = false; return false; },