From 27007efa265c2b0cb540300d284d405ae4892d77 Mon Sep 17 00:00:00 2001 From: Roie Natan Date: Tue, 13 Oct 2020 19:10:47 +0300 Subject: [PATCH 1/6] init --- src/assets/locales/en/translation.json | 9 +- .../Proposal/Create/CreateProposal.scss | 28 +- .../Proposal/Create/PluginForms/ABIService.ts | 8 +- .../CreateGenericMultiCallProposal.tsx | 496 ++++++++++++++++++ .../Proposal/Create/PluginForms/Validators.ts | 51 +- 5 files changed, 584 insertions(+), 8 deletions(-) create mode 100644 src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index e0a1119e..46230ec1 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -47,6 +47,8 @@ "Amount Redeemed": "Amount Redeemed:", "No Results": "No Results", "Search": "Search", + "Required": "Required", + "Valide Non-Negative" : "Please enter a non-negative value", "Validate Address" : "Please enter a valid address", "Validate HEX" : "Must be a hex value", "Validate Digits" : "Must contain only digits", @@ -55,5 +57,10 @@ "No Write Methods": "No write methods found for target ", "contract": "contract", "Add Params": "Add Params", - "Remove Params": "Remove Params" + "Remove Params": "Remove Params", + "Add contract": "Add contract", + "Contract Add Success": "Contract added successfully!", + "Contract Exist": "Contract already exist!", + "Choose contract": "Choose contract", + "Choose method": "Choose method" } diff --git a/src/components/Proposal/Create/CreateProposal.scss b/src/components/Proposal/Create/CreateProposal.scss index b2691ae9..f5c991e4 100644 --- a/src/components/Proposal/Create/CreateProposal.scss +++ b/src/components/Proposal/Create/CreateProposal.scss @@ -8,7 +8,25 @@ } fieldset { - margin: 10px 0px; + margin: 30px 0px; +} + +.removeFieldSet { + float: right; + border: none; + color: $accent-2; + background: none; +} + +.addFieldSet { + border: none; + background: none; + color: $sky; +} + +.removeFieldSet:hover, +.addFieldSet:hover { + opacity: 0.7; } .createProposalWrapper { @@ -67,6 +85,7 @@ fieldset { } form { + select, input { width: 100%; margin-top: 3px; @@ -123,11 +142,16 @@ fieldset { } } + .addContract { + position: relative; + } + .encodedData { overflow-wrap: break-word; - margin: 5px 0 40px 0px; + margin: 5px 0 5px 0px; // margin: 5px 0 40px 0px; TEMP border: 1px solid; padding: 20px; + max-width: 480px; // TEMPORARY - NEED TO BE DYNAMIC } .proposerIsAdminCheckbox { diff --git a/src/components/Proposal/Create/PluginForms/ABIService.ts b/src/components/Proposal/Create/PluginForms/ABIService.ts index 38d4c602..ba7bbb41 100644 --- a/src/components/Proposal/Create/PluginForms/ABIService.ts +++ b/src/components/Proposal/Create/PluginForms/ABIService.ts @@ -83,18 +83,18 @@ export const extractABIMethods = (abi: AbiItem[]): IAbiItemExtended[] => { */ export const validateABIInputs = (data: Array): boolean => { for (const input of data) { - switch (input.type) { - case "address": + switch (true) { + case input.type.includes("address"): if (!isAddress(input.value)) { return false; } break; - case "bytes": + case input.type.includes("byte"): if (!isHexString(input.value)) { return false; } break; - case "uint256": + case input.type.includes("uint"): if (/^\d+$/.test(input.value) === false) { return false; } diff --git a/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx b/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx new file mode 100644 index 00000000..a361cc50 --- /dev/null +++ b/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx @@ -0,0 +1,496 @@ +import { IPluginState } from "@daostack/arc.js"; +import { createProposal } from "actions/arcActions"; +import { enableWalletProvider } from "arc"; +import { ErrorMessage, Field, Form, Formik, FormikProps, FieldArray } from "formik"; +import Analytics from "lib/analytics"; +import * as React from "react"; +import { connect } from "react-redux"; +import { showNotification } from "reducers/notifications"; +import { baseTokenName, isValidUrl, linkToEtherScan, isAddress } from "lib/util"; +import TagsSelector from "components/Proposal/Create/PluginForms/TagsSelector"; +import TrainingTooltip from "components/Shared/TrainingTooltip"; +import * as css from "../CreateProposal.scss"; +import MarkdownField from "./MarkdownField"; +import HelpButton from "components/Shared/HelpButton"; +import i18next from "i18next"; +import { IFormModalService, CreateFormModalService } from "components/Shared/FormModalService"; +import ResetFormButton from "components/Proposal/Create/PluginForms/ResetFormButton"; +import { getABIByContract, extractABIMethods, encodeABI } from "./ABIService"; +import Loading from "components/Shared/Loading"; +import { any } from "prop-types"; +import { requireValue, validateParam } from "./Validators"; + +const contracts = [ + "0x543Ff227F64Aa17eA132Bf9886cAb5DB55DCAddf", + "0x5C5cbaC45b18F990AbcC4b890Bf98d82e9ee58A0", + "0x24832a7A5408B2b18e71136547d308FCF60B6e71", + "0x4E073a7E4a2429eCdfEb1324a472dd8e82031F34", +]; + +const initialValues = { + contracts: [ + { + address: "", + value: 0, + abi: any, + method: "", + methods: [] as any, + params: [] as any, + values: [] as any, + callData: "", + }, + ], +}; + +interface IExternalProps { + daoAvatarAddress: string; + handleClose: () => any; + pluginState: IPluginState; +} + +interface IDispatchProps { + createProposal: typeof createProposal; + showNotification: typeof showNotification; +} + +interface IAddContractStatus { + error: string + message: string +} + +interface IStateProps { + loading: boolean + tags: Array + addContractStatus: IAddContractStatus + contracts: Array +} + +// interface IABIField { +// id: string, +// name: string +// type: string +// inputType: string, +// placeholder: string, +// } + +type IProps = IExternalProps & IDispatchProps; + +const mapDispatchToProps = { + createProposal, + showNotification, +}; + +interface IFormValues { + description: string; + title: string; + url: string; + contracts: [] + [key: string]: any; +} + +const defaultValues: IFormValues = Object.freeze({ + description: "", + title: "", + url: "", + tags: [], + contracts: [], +}); + + +class CreateGenericMultiCallProposal extends React.Component { + + formModalService: IFormModalService; + currentFormValues: IFormValues; + + constructor(props: IProps) { + super(props); + this.state = { loading: false, addContractStatus: { error: "", message: "" }, contracts: contracts, tags: [] }; + + this.handleSubmit = this.handleSubmit.bind(this); + this.formModalService = CreateFormModalService( + "CreateGenericMultiCallProposal", + defaultValues, + () => Object.assign(this.currentFormValues, this.state), + (formValues: IFormValues, firstTime: boolean) => { + this.currentFormValues = formValues; + if (firstTime) { + Object.assign(this.state, { + tags: formValues.tags, + }); + } + else { this.setState({ tags: formValues.tags }); } + }, + this.props.showNotification); + } + + componentWillUnmount() { + this.formModalService.saveCurrentValues(); + } + + public async handleSubmit(values: IFormValues, { setSubmitting }: any): Promise { + if (!await enableWalletProvider({ showNotification: this.props.showNotification })) { return; } + + const proposalValues = { + ...values, + dao: this.props.daoAvatarAddress, + plugin: this.props.pluginState.address, + tags: this.state.tags, + }; + + setSubmitting(false); + await this.props.createProposal(proposalValues); + + Analytics.track("Submit Proposal", { + "DAO Address": this.props.daoAvatarAddress, + "Proposal Title": values.title, + "Plugin Address": this.props.pluginState.address, + "Plugin Name": this.props.pluginState.name, + }); + + this.props.handleClose(); + } + + private onTagsChange = (tags: any[]): void => { + this.setState({ tags }); + } + + /** + * Given a contract address, checks whether it's valid, not exists in the current contract list and that the contract is verified with valid ABI data and write methods. + * If all checks are okay, pushes the contract address to the contract lists, otherwise returns a appropriate message. + * @param {string} contractToCall + */ + private verifyContract = async (contractToCall: string) => { + const addContractStatus = { + error: "", + message: "", + }; + + if (!isAddress(contractToCall)) { + addContractStatus.error = "NOT_VALID_ADDRESS"; + addContractStatus.message = i18next.t("Validate Address"); + } else if (this.state.contracts.includes(contractToCall)) { + addContractStatus.error = "CONTRACT_EXIST"; + addContractStatus.message = i18next.t("Contract Exist"); + } else { + this.setState({ loading: true }); + const abiData = await getABIByContract(contractToCall); + const abiMethods = extractABIMethods(abiData); + if (abiMethods.length > 0) { + this.state.contracts.push(contractToCall); + addContractStatus.error = ""; + addContractStatus.message = i18next.t("Contract Add Success"); + } else { + addContractStatus.error = "ABI_DATA_ERROR"; + addContractStatus.message = abiData.length === 0 ? i18next.t("No ABI") : i18next.t("No Write Methods"); + } + } + this.setState({ loading: false, addContractStatus: addContractStatus }); + } + + private getContractABI = async (contractToCall: string, setFieldValue: any, index: number) => { + setFieldValue(`contracts.${index}.method`, ""); // reset + setFieldValue(`contracts.${index}.callData`, ""); // reset + const abiData = await getABIByContract(contractToCall); + const abiMethods = extractABIMethods(abiData); + setFieldValue(`contracts.${index}.abi`, abiData); + setFieldValue(`contracts.${index}.methods`, abiMethods); + } + + private getMethodInputs = (abi: any, methods: any[], methodName: any, setFieldValue: any, index: number) => { + setFieldValue(`contracts.${index}.callData`, ""); // reset + const selectedMethod = methods.filter(method => method.methodSignature === methodName); + const abiParams = selectedMethod[0].inputs.map((input: any, index: number) => { + return { + id: index, + name: input.name, + type: input.type, + inputType: input.type === "bool" ? "number" : "text", + placeholder: `${input.name} (${input.type})`, + methodSignature: input.methodSignature, + }; + }); + setFieldValue(`contracts.${index}.params`, abiParams); + if (abiParams.length === 0) { + const encodedData = encodeABI(abi, methodName, []); + setFieldValue(`contracts.${index}.callData`, encodedData); + } + } + + private abiInputChange = (abi: any, values: any, name: string, params: any, setFieldValue: any, index: number) => { + const abiValues = []; + for (const [i, abiInput] of params.entries()) { + abiValues.push({ type: abiInput.type, value: values[i] }); + } + + const encodedData = encodeABI(abi, name, abiValues); + setFieldValue(`contracts.${index}.callData`, encodedData); + } + + + public render(): RenderOutput { + const { handleClose } = this.props; + const { loading, addContractStatus } = this.state; + + const addressesOptions = contracts.map((address, index) => { + return ; + }); + + return ( +
+ + { + const errors: any = {}; + + this.currentFormValues = values; + + const require = (name: string) => { + if (!(values as any)[name]) { + errors[name] = "Required"; + } + }; + + if (!isValidUrl(values.url)) { + errors.url = "Invalid URL"; + } + + require("title"); + require("description"); + + return errors; + }} + onSubmit={this.handleSubmit} + // eslint-disable-next-line react/jsx-no-bind + render={({ + errors, + touched, + handleBlur, + isSubmitting, + resetForm, + values, + setFieldValue, + }: FormikProps) => +
+ + + + + + + + + { setFieldValue("description", value); }} + id="descriptionInput" + placeholder={i18next.t("Description Placeholder")} + name="description" + className={touched.description && errors.description ? css.error : null} + /> + + + + + +
+ +
+ + + + + + +
+ + { setFieldValue("addContract", e.target.value); this.verifyContract(e.target.value); }} + disabled={loading ? true : false} + /> + {loading ? : addContractStatus.error === "ABI_DATA_ERROR" ? +
+ {addContractStatus.message} + {i18next.t("contract")} +
: addContractStatus.message} +
+ + + {({ insert, remove, push }) => ( +
+ { + values.contracts.length > 0 && values.contracts.map((contract: any, index: any) => ( +
+ + +
+ + +
+ +
+ + { setFieldValue(`contracts.${index}.address`, e.target.value); await this.getContractABI(e.target.value, setFieldValue, index) }} + component="select" + name={`contracts.${index}.address`} + placeholder="Select contract" + type="text" + validate={requireValue} + //className={(touched.contracts[index] as any).address && (errors.contracts[index] as any).address ? css.error : null} + > + + {addressesOptions} + +
+ + { + (values.contracts[index] as any).address !== "" && +
+ + {(values.contracts[index] as any)?.methods?.length === 0 ? "loading..." : + { setFieldValue(`contracts.${index}.method`, e.target.value); this.getMethodInputs((values.contracts[index] as any)?.abi, (values.contracts[index] as any)?.methods, e.target.value, setFieldValue, index) }} + component="select" + name={`contracts.${index}.method`} + placeholder="Select method" + type="text" + validate={requireValue} + //className={(touched.contracts[index] as any).method && (errors.contracts[index] as any).method ? css.error : null} + > + + {(values.contracts[index] as any)?.methods?.map((method: any, j: any) => ( + + ))} + } +
+ } + + { + (values.contracts[index] as any).method !== "" && +
+ {(values.contracts[index] as any).params.map((param: any, i: number) => ( + + + { handleBlur(e); this.abiInputChange((values.contracts[index] as any).abi, (values.contracts[index] as any).values, (values.contracts[index] as any).method, (values.contracts[index] as any).params, setFieldValue, index); }} + validate={(e: any) => validateParam(param.type, e)} + //className={(touched.contracts[index] as any).values[i] && (errors.contracts[index] as any).values[i] ? css.error : null} + /> + + ))} +
+ } + + +
{(values.contracts[index] as any).callData}
+
+ )) + } + +
+ ) + } +
+ +
+ + + + + + + + + + +
+ + } + /> + +
+
+ ); + } +} + +export default connect(null, mapDispatchToProps)(CreateGenericMultiCallProposal); diff --git a/src/components/Proposal/Create/PluginForms/Validators.ts b/src/components/Proposal/Create/PluginForms/Validators.ts index 8e123d13..ab8cc7cf 100644 --- a/src/components/Proposal/Create/PluginForms/Validators.ts +++ b/src/components/Proposal/Create/PluginForms/Validators.ts @@ -1,4 +1,6 @@ -import { isAddress } from "../../../../lib/util"; +import { isAddress } from "lib/util"; +import i18next from "i18next"; +import { isHexString } from "ethers/utils"; const constants = { REQUIRED: "Required", @@ -116,3 +118,50 @@ export const address = (value: string, allowNulls = false): string => { } return error; }; + + +/** + * Given a value returns error message in case value is less than 0 or no value provided + * @param {any} value + */ +export const requireValue = (value: any): string => { + let error; + if (value === "") { + error = i18next.t("Required"); + } else if (value < 0) { + error = i18next.t("Valide Non-Negative"); + } + return error; +}; + +/** + * Given ABI method param type (address, byets32, unit256, ...) and it's value, returns error message in case validation fails or no value provided + * @param {string} type + * @param {any} value + */ +export const validateParam = (type: string, value: any): string => { + let error; + if (!value) { + error = i18next.t("Required"); + } + else { + switch (true) { + case type.includes("address"): + if (!isAddress(value)) { + error = i18next.t("Validate Address"); + } + break; + case type.includes("byte"): + if (!isHexString(value)) { + error = i18next.t("Validate HEX"); + } + break; + case type.includes("uint"): + if (/^\d+$/.test(value) === false) { + error = i18next.t("Validate Digits"); + } + break; + } + } + return error; +}; From 122787038e5d4a44d91a3dcd2668d7f902f4e4d2 Mon Sep 17 00:00:00 2001 From: Roie Natan Date: Tue, 13 Oct 2020 23:15:47 +0300 Subject: [PATCH 2/6] merge dev-2 --- src/assets/locales/en/translation.json | 7 +++++++ .../Create/PluginForms/CreateGenericMultiCallProposal.tsx | 2 +- src/components/Proposal/Create/PluginForms/Validators.ts | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index df53e4d2..efaaa61e 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -47,6 +47,8 @@ "Amount Redeemed": "Amount Redeemed:", "No Results": "No Results", "Search": "Search", + "Required": "Required", + "Validate Non-Negative" : "Please enter a non-negative value", "Validate Address": "Please enter a valid address", "Validate HEX": "Must be a hex value", "Validate Digits": "Must contain only digits", @@ -56,6 +58,11 @@ "contract": "contract", "Add Params": "Add Params", "Remove Params": "Remove Params", + "Choose contract": "Choose contract", + "Choose method": "Choose method", + "Contract Exist": "Contract already exist!", + "Contract Add Success": "Contract added successfully!", + "Add contract": "Add contract", "CommentHelpText": "Type your comments below.\n• Paste youtube or vimeo links to embed videos\n• Paste image links to embed images\n• Type in Markdown if you are feeling nerdish", "JoinTheConversation": "Join the Conversation", "StartAConversation": "Start a Conversation", diff --git a/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx b/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx index a361cc50..bc6f1a78 100644 --- a/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx +++ b/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx @@ -156,7 +156,7 @@ class CreateGenericMultiCallProposal extends React.Component { diff --git a/src/components/Proposal/Create/PluginForms/Validators.ts b/src/components/Proposal/Create/PluginForms/Validators.ts index ab8cc7cf..0fb4341c 100644 --- a/src/components/Proposal/Create/PluginForms/Validators.ts +++ b/src/components/Proposal/Create/PluginForms/Validators.ts @@ -129,7 +129,7 @@ export const requireValue = (value: any): string => { if (value === "") { error = i18next.t("Required"); } else if (value < 0) { - error = i18next.t("Valide Non-Negative"); + error = i18next.t("Validate Non-Negative"); } return error; }; From b658ac186c25ceb109b716da0021a699075cb986 Mon Sep 17 00:00:00 2001 From: Roie Natan Date: Tue, 13 Oct 2020 23:30:36 +0300 Subject: [PATCH 3/6] fix merge from dev-2 of transelation.json --- src/assets/locales/en/translation.json | 148 ++++++++++++------------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index efaaa61e..ef2d1207 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -1,76 +1,76 @@ { - "Edit Home Page": "Edit Home Page", - "Feed Tooltip": "See a feed of recent updates to DAOs you follow", - "Members Tooltip": "List of entities (DAOs and individuals) that have voting power in the DAO", - "Toggle Tooltips Tooltip": "Show / hide tooltips on hover", - "Connect Tooltip": "Click here to connect your wallet provider", - "New Proposal Button Tooltip": "A small amount of ETH is necessary to submit a proposal in order to pay gas costs", - "Information Tab Tooltip": "Learn about the protocol parameters for this scheme", - "Regular Proposal Tooltip": "Regular proposals are passed or failed via absolute majority over a configured voting period. If enough GEN is staked predicting they will pass, they can move to the pending and then boosted queues.", - "Pending Proposal Tooltip": "Pending boosting proposals have reached the prediction score required for boosting and now must make it through the pending period without dipping below that threshold in order to be boosted.", - "Boosted Proposal Tooltip": "Boosted proposals are passed or failed via relative majority over a configured voting period", - "Title Tooltip": "The title is the header of the proposal card and will be the first visible information about your proposal", - "Description Tooltip": "Short description of the proposal.\n • What are you proposing to do?\n • Why is it important?\n • How much will it cost the DAO?\n • When do you plan to deliver the work?", - "Help Button Tooltip": "• Paste youtube or vimeo links to embed videos\n• Paste image links to embed images\n• Type in Markdown if you are feeling nerdish", - "Tags Tooltip": "Add some tags to give context about your proposal e.g. idea, signal, bounty, research, etc", - "URL Tooltip": "Link to the fully detailed description of your proposal", - "Submit Proposal Tooltip": "Once the proposal is submitted it cannot be edited or deleted", - "Export Proposal Tooltip": "Export proposal", - "Title Placeholder": "Summarize your proposal", - "Description Placeholder": "Describe your proposal in greater detail", - "URL Placeholder": "Description URL", - "In Clipboard": "Copied to clipboard", - "Email Alerts": "Email Alerts", - "Alchemy Alpha Message": "Alchemy and Arc are in Alpha. There will be BUGS! We don't guarantee complete security. *Play at your own risk*", - "Clear entries": "Clear Entries", - "Sort by": "Sort by:", - "Name": "Name", - "ETH Balance": "ETH Balance", - "Members": "Members", - "Plugin activation time": "The plugin will be activated at", - "Total Holdings": "Total Holdings", - "Create A DAO": "Create A DAO", - "Your DAOs": "Your DAOs", - "Other DAOs": "Other DAOs", - "All DAOs": "All DAOs", - "Open Proposals": "Open Proposals", - "3BoxLoginSuccess": "Logged in to 3Box", - "3BoxProfileSuccess": "Profile data saved to 3Box", - "Following": "Now following", - "UnFollowing": "No longer following", - "Raw call data": "Raw call data", - "Address": "Address:", - "Funding": "Funding:", - "Reputation Minted": "Reputation Minted:", - "Minimum DAO bounty": "Minimum DAO bounty:", - "Amount": "Amount:", - "Amount Redeemed": "Amount Redeemed:", - "No Results": "No Results", - "Search": "Search", - "Required": "Required", - "Validate Non-Negative" : "Please enter a non-negative value", - "Validate Address": "Please enter a valid address", - "Validate HEX": "Must be a hex value", - "Validate Digits": "Must contain only digits", - "Call to Contract": "This proposal will call the following ", - "No ABI": "No ABI found for target contract, please verify the ", - "No Write Methods": "No write methods found for target ", - "contract": "contract", - "Add Params": "Add Params", - "Remove Params": "Remove Params", - "Choose contract": "Choose contract", - "Choose method": "Choose method", - "Contract Exist": "Contract already exist!", - "Contract Add Success": "Contract added successfully!", - "Add contract": "Add contract", - "CommentHelpText": "Type your comments below.\n• Paste youtube or vimeo links to embed videos\n• Paste image links to embed images\n• Type in Markdown if you are feeling nerdish", - "JoinTheConversation": "Join the Conversation", - "StartAConversation": "Start a Conversation", - "Submit3BoxPost": "Submit", - "Enable3BoxInteractions": "Post and delete comments", - "CreateFirst3BoxPost": "Post the first comment", - "Delete3BoxPost": "Delete this comment...", - "Delete3BoxPostConfirmation": "Delete this comment?", - "Yes": "Yes", - "No": "No" + "Edit Home Page": "Edit Home Page", + "Feed Tooltip": "See a feed of recent updates to DAOs you follow", + "Members Tooltip": "List of entities (DAOs and individuals) that have voting power in the DAO", + "Toggle Tooltips Tooltip": "Show / hide tooltips on hover", + "Connect Tooltip": "Click here to connect your wallet provider", + "New Proposal Button Tooltip": "A small amount of ETH is necessary to submit a proposal in order to pay gas costs", + "Information Tab Tooltip": "Learn about the protocol parameters for this scheme", + "Regular Proposal Tooltip": "Regular proposals are passed or failed via absolute majority over a configured voting period. If enough GEN is staked predicting they will pass, they can move to the pending and then boosted queues.", + "Pending Proposal Tooltip": "Pending boosting proposals have reached the prediction score required for boosting and now must make it through the pending period without dipping below that threshold in order to be boosted.", + "Boosted Proposal Tooltip": "Boosted proposals are passed or failed via relative majority over a configured voting period", + "Title Tooltip": "The title is the header of the proposal card and will be the first visible information about your proposal", + "Description Tooltip": "Short description of the proposal.\n • What are you proposing to do?\n • Why is it important?\n • How much will it cost the DAO?\n • When do you plan to deliver the work?", + "Help Button Tooltip": "• Paste youtube or vimeo links to embed videos\n• Paste image links to embed images\n• Type in Markdown if you are feeling nerdish", + "Tags Tooltip": "Add some tags to give context about your proposal e.g. idea, signal, bounty, research, etc", + "URL Tooltip": "Link to the fully detailed description of your proposal", + "Submit Proposal Tooltip": "Once the proposal is submitted it cannot be edited or deleted", + "Export Proposal Tooltip": "Export proposal", + "Title Placeholder": "Summarize your proposal", + "Description Placeholder": "Describe your proposal in greater detail", + "URL Placeholder": "Description URL", + "In Clipboard": "Copied to clipboard", + "Email Alerts": "Email Alerts", + "Alchemy Alpha Message": "Alchemy and Arc are in Alpha. There will be BUGS! We don't guarantee complete security. *Play at your own risk*", + "Clear entries": "Clear Entries", + "Sort by": "Sort by:", + "Name": "Name", + "ETH Balance": "ETH Balance", + "Members": "Members", + "Plugin activation time": "The plugin will be activated at", + "Total Holdings": "Total Holdings", + "Create A DAO": "Create A DAO", + "Your DAOs": "Your DAOs", + "Other DAOs": "Other DAOs", + "All DAOs": "All DAOs", + "Open Proposals": "Open Proposals", + "3BoxLoginSuccess": "Logged in to 3Box", + "3BoxProfileSuccess": "Profile data saved to 3Box", + "Following": "Now following", + "UnFollowing": "No longer following", + "Raw call data": "Raw call data", + "Address": "Address:", + "Funding": "Funding:", + "Reputation Minted": "Reputation Minted:", + "Minimum DAO bounty": "Minimum DAO bounty:", + "Amount": "Amount:", + "Amount Redeemed": "Amount Redeemed:", + "No Results": "No Results", + "Search": "Search", + "Validate Address": "Please enter a valid address", + "Validate HEX": "Must be a hex value", + "Validate Digits": "Must contain only digits", + "Call to Contract": "This proposal will call the following ", + "No ABI": "No ABI found for target contract, please verify the ", + "No Write Methods": "No write methods found for target ", + "contract": "contract", + "Add Params": "Add Params", + "Remove Params": "Remove Params", + "CommentHelpText": "Type your comments below.\n• Paste youtube or vimeo links to embed videos\n• Paste image links to embed images\n• Type in Markdown if you are feeling nerdish", + "JoinTheConversation": "Join the Conversation", + "StartAConversation": "Start a Conversation", + "Submit3BoxPost": "Submit", + "Enable3BoxInteractions": "Post and delete comments", + "CreateFirst3BoxPost": "Post the first comment", + "Delete3BoxPost": "Delete this comment...", + "Delete3BoxPostConfirmation": "Delete this comment?", + "Yes": "Yes", + "No": "No", + "Required": "Required", + "Validate Non-Negative" : "Please enter a non-negative value", + "Add contract": "Add contract", + "Contract Add Success": "Contract added successfully!", + "Contract Exist": "Contract already exist!", + "Choose contract": "Choose contract", + "Choose method": "Choose method" } From fa334d26c3d1090f680c9744dc70c8fdabf4593f Mon Sep 17 00:00:00 2001 From: Roie Natan Date: Wed, 14 Oct 2020 17:33:13 +0300 Subject: [PATCH 4/6] Fix saving user inputs when closing the modal ; Fix lint errors ; Added select groups ; Added interfaces --- src/assets/locales/en/translation.json | 8 +- .../CreateGenericMultiCallProposal.tsx | 167 +++++++++++------- .../Proposal/Create/PluginForms/Validators.ts | 5 +- 3 files changed, 113 insertions(+), 67 deletions(-) diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index ef2d1207..98282bed 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -72,5 +72,11 @@ "Contract Add Success": "Contract added successfully!", "Contract Exist": "Contract already exist!", "Choose contract": "Choose contract", - "Choose method": "Choose method" + "Choose method": "Choose method", + "Loading": "Loading...", + "Remove": "Remove", + "Cancel": "Cancel", + "Submit proposal": "Submit proposal", + "Whitelisted contracts": "Whitelisted contracts", + "User contracts": "User contracts" } diff --git a/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx b/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx index bc6f1a78..3538d62b 100644 --- a/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx +++ b/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx @@ -17,31 +17,15 @@ import { IFormModalService, CreateFormModalService } from "components/Shared/For import ResetFormButton from "components/Proposal/Create/PluginForms/ResetFormButton"; import { getABIByContract, extractABIMethods, encodeABI } from "./ABIService"; import Loading from "components/Shared/Loading"; -import { any } from "prop-types"; import { requireValue, validateParam } from "./Validators"; -const contracts = [ +const whitelistedContracts = [ "0x543Ff227F64Aa17eA132Bf9886cAb5DB55DCAddf", "0x5C5cbaC45b18F990AbcC4b890Bf98d82e9ee58A0", "0x24832a7A5408B2b18e71136547d308FCF60B6e71", "0x4E073a7E4a2429eCdfEb1324a472dd8e82031F34", ]; -const initialValues = { - contracts: [ - { - address: "", - value: 0, - abi: any, - method: "", - methods: [] as any, - params: [] as any, - values: [] as any, - callData: "", - }, - ], -}; - interface IExternalProps { daoAvatarAddress: string; handleClose: () => any; @@ -62,14 +46,14 @@ interface IStateProps { loading: boolean tags: Array addContractStatus: IAddContractStatus - contracts: Array + whitelistedContracts: Array + userContracts: Array } // interface IABIField { // id: string, // name: string // type: string -// inputType: string, // placeholder: string, // } @@ -80,11 +64,22 @@ const mapDispatchToProps = { showNotification, }; +interface IContract { + address: string // Contract address + value: number // Token to send with the proposal + abi: any // Contract ABI data + methods: any // ABI write methods + method: string // Selected method + params: any // Method params + values: any // Params values + callData: string // The encoded data +} + interface IFormValues { description: string; title: string; url: string; - contracts: [] + contracts: Array [key: string]: any; } @@ -93,7 +88,19 @@ const defaultValues: IFormValues = Object.freeze({ title: "", url: "", tags: [], - contracts: [], + userContracts: [], + contracts: [ + { + address: "", + value: 0, + abi: [] as any, + methods: [] as any, + method: "", + params: [] as any, + values: [] as any, + callData: "", + }, + ], }); @@ -104,21 +111,25 @@ class CreateGenericMultiCallProposal extends React.Component Object.assign(this.currentFormValues, this.state), + () => Object.assign(this.currentFormValues, this.state), // this.removeABIDataFromObject(this.currentFormValues) (formValues: IFormValues, firstTime: boolean) => { + // for (const contract of formValues.contracts) { + // contract.abi = await getABIByContract(contract.address); + // } this.currentFormValues = formValues; if (firstTime) { Object.assign(this.state, { tags: formValues.tags, + userContracts: formValues.userContracts, }); } - else { this.setState({ tags: formValues.tags }); } + else { this.setState({ tags: formValues.tags, userContracts: formValues.userContracts }); } }, this.props.showNotification); } @@ -127,11 +138,25 @@ class CreateGenericMultiCallProposal extends React.Component { + public async handleSubmit(formValues: IFormValues, { setSubmitting }: any): Promise { if (!await enableWalletProvider({ showNotification: this.props.showNotification })) { return; } + const contractsToCall = []; + const callsData = []; + const values = []; + + for (const contract of formValues.contracts) { + contractsToCall.push(contract.address); + callsData.push(contract.callData); + values.push(contract.value); + } + const proposalValues = { - ...values, + title: formValues.title, + description: formValues.description, + contractsToCall: contractsToCall, + callsData: callsData, + values: values, dao: this.props.daoAvatarAddress, plugin: this.props.pluginState.address, tags: this.state.tags, @@ -142,7 +167,7 @@ class CreateGenericMultiCallProposal extends React.Component { + // for (const contract of obj.contracts) { + // contract.abi = []; + // } + // return obj; + // } + private onTagsChange = (tags: any[]): void => { this.setState({ tags }); } @@ -168,7 +200,7 @@ class CreateGenericMultiCallProposal extends React.Component 0) { - this.state.contracts.push(contractToCall); + this.state.userContracts.push(contractToCall); addContractStatus.error = ""; addContractStatus.message = i18next.t("Contract Add Success"); } else { @@ -204,15 +236,13 @@ class CreateGenericMultiCallProposal extends React.Component { + return ; + }); - const addressesOptions = contracts.map((address, index) => { + const userContractsOptions = userContracts.map((address, index) => { return ; }); @@ -240,7 +274,7 @@ class CreateGenericMultiCallProposal extends React.Component { const errors: any = {}; @@ -355,12 +389,13 @@ class CreateGenericMultiCallProposal extends React.Component - {({ insert, remove, push }) => ( + {({ insert, remove, push }) => ( // eslint-disable-line @typescript-eslint/no-unused-vars
{ values.contracts.length > 0 && values.contracts.map((contract: any, index: any) => ( -
- +
+ {/* eslint-disable-next-line react/jsx-no-bind */} + {values.contracts.length > 1 && }
@@ -384,40 +417,45 @@ class CreateGenericMultiCallProposal extends React.Component{(msg) => {msg}} - { setFieldValue(`contracts.${index}.address`, e.target.value); await this.getContractABI(e.target.value, setFieldValue, index) }} + { setFieldValue(`contracts.${index}.address`, e.target.value); await this.getContractABI(e.target.value, setFieldValue, index); }} component="select" name={`contracts.${index}.address`} placeholder="Select contract" type="text" validate={requireValue} - //className={(touched.contracts[index] as any).address && (errors.contracts[index] as any).address ? css.error : null} > - {addressesOptions} + + {whitelistedContractsOptions} + + {userContractsOptions.length > 0 && + + {userContractsOptions} + + }
{ - (values.contracts[index] as any).address !== "" && + values.contracts[index].address !== "" &&
- {(values.contracts[index] as any)?.methods?.length === 0 ? "loading..." : - { setFieldValue(`contracts.${index}.method`, e.target.value); this.getMethodInputs((values.contracts[index] as any)?.abi, (values.contracts[index] as any)?.methods, e.target.value, setFieldValue, index) }} + {values.contracts[index]?.methods?.length === 0 ? i18next.t("Loading") : + { setFieldValue(`contracts.${index}.method`, e.target.value); this.getMethodInputs(values.contracts[index].abi, values.contracts[index]?.methods, e.target.value, setFieldValue, index); }} component="select" name={`contracts.${index}.method`} placeholder="Select method" type="text" validate={requireValue} - //className={(touched.contracts[index] as any).method && (errors.contracts[index] as any).method ? css.error : null} > - {(values.contracts[index] as any)?.methods?.map((method: any, j: any) => ( + {values.contracts[index]?.methods?.map((method: any, j: any) => ( ))} } @@ -425,10 +463,10 @@ class CreateGenericMultiCallProposal extends React.Component - {(values.contracts[index] as any).params.map((param: any, i: number) => ( - + values.contracts[index].method !== "" && +
+ {values.contracts[index].params.map((param: any, i: number) => ( + ))} @@ -449,14 +488,15 @@ class CreateGenericMultiCallProposal extends React.ComponentEncoded Data -
{(values.contracts[index] as any).callData}
+
{values.contracts[index].callData}
)) } + // eslint-disable-next-line react/jsx-no-bind + onClick={() => push(defaultValues.contracts[0])}>+ Add Another Contract
) } @@ -469,8 +509,8 @@ class CreateGenericMultiCallProposal extends React.Component + {i18next.t("Cancel")} + + {i18next.t("Submit proposal")} +
} /> - ); diff --git a/src/components/Proposal/Create/PluginForms/Validators.ts b/src/components/Proposal/Create/PluginForms/Validators.ts index 0fb4341c..75dbc4e9 100644 --- a/src/components/Proposal/Create/PluginForms/Validators.ts +++ b/src/components/Proposal/Create/PluginForms/Validators.ts @@ -124,6 +124,7 @@ export const address = (value: string, allowNulls = false): string => { * Given a value returns error message in case value is less than 0 or no value provided * @param {any} value */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const requireValue = (value: any): string => { let error; if (value === "") { @@ -137,9 +138,9 @@ export const requireValue = (value: any): string => { /** * Given ABI method param type (address, byets32, unit256, ...) and it's value, returns error message in case validation fails or no value provided * @param {string} type - * @param {any} value + * @param {string} value */ -export const validateParam = (type: string, value: any): string => { +export const validateParam = (type: string, value: string): string => { let error; if (!value) { error = i18next.t("Required"); From 4342ba69b1dd823bc16c3094777e8c10f640a226 Mon Sep 17 00:00:00 2001 From: Roie Natan Date: Thu, 15 Oct 2020 16:30:29 +0300 Subject: [PATCH 5/6] Improvments to single call generic scheme ; Removed SelectSearch component ; Minor changes to multi-call generic scheme --- .../CreateGenericMultiCallProposal.tsx | 16 +- .../CreateUnknownGenericPluginProposal.tsx | 149 ++++++++---------- src/components/Shared/SelectSearch.scss | 57 ------- src/components/Shared/SelectSearch.tsx | 86 ---------- 4 files changed, 71 insertions(+), 237 deletions(-) delete mode 100644 src/components/Shared/SelectSearch.scss delete mode 100644 src/components/Shared/SelectSearch.tsx diff --git a/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx b/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx index 3538d62b..97206930 100644 --- a/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx +++ b/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx @@ -119,9 +119,6 @@ class CreateGenericMultiCallProposal extends React.Component Object.assign(this.currentFormValues, this.state), // this.removeABIDataFromObject(this.currentFormValues) (formValues: IFormValues, firstTime: boolean) => { - // for (const contract of formValues.contracts) { - // contract.abi = await getABIByContract(contract.address); - // } this.currentFormValues = formValues; if (firstTime) { Object.assign(this.state, { @@ -134,6 +131,12 @@ class CreateGenericMultiCallProposal extends React.Component { setFieldValue(`contracts.${index}.address`, e.target.value); await this.getContractABI(e.target.value, setFieldValue, index); }} component="select" name={`contracts.${index}.address`} - placeholder="Select contract" type="text" validate={requireValue} > - + {whitelistedContractsOptions} @@ -450,11 +453,10 @@ class CreateGenericMultiCallProposal extends React.Component { setFieldValue(`contracts.${index}.method`, e.target.value); this.getMethodInputs(values.contracts[index].abi, values.contracts[index]?.methods, e.target.value, setFieldValue, index); }} component="select" name={`contracts.${index}.method`} - placeholder="Select method" type="text" validate={requireValue} > - + {values.contracts[index]?.methods?.map((method: any, j: any) => ( ))} diff --git a/src/components/Proposal/Create/PluginForms/CreateUnknownGenericPluginProposal.tsx b/src/components/Proposal/Create/PluginForms/CreateUnknownGenericPluginProposal.tsx index d74be61c..9d91d953 100644 --- a/src/components/Proposal/Create/PluginForms/CreateUnknownGenericPluginProposal.tsx +++ b/src/components/Proposal/Create/PluginForms/CreateUnknownGenericPluginProposal.tsx @@ -6,11 +6,9 @@ import Analytics from "lib/analytics"; import * as React from "react"; import { connect } from "react-redux"; import { showNotification } from "reducers/notifications"; -import { baseTokenName, isValidUrl, isAddress } from "lib/util"; -import { isHexString } from "ethers/utils"; +import { baseTokenName, isValidUrl, linkToEtherScan } from "lib/util"; import TagsSelector from "components/Proposal/Create/PluginForms/TagsSelector"; import TrainingTooltip from "components/Shared/TrainingTooltip"; -import { SelectSearch } from "components/Shared/SelectSearch"; import * as css from "../CreateProposal.scss"; import MarkdownField from "./MarkdownField"; import HelpButton from "components/Shared/HelpButton"; @@ -19,7 +17,7 @@ import { IFormModalService, CreateFormModalService } from "components/Shared/For import ResetFormButton from "components/Proposal/Create/PluginForms/ResetFormButton"; import { getABIByContract, extractABIMethods, encodeABI } from "./ABIService"; import Loading from "components/Shared/Loading"; -import { linkToEtherScan } from "lib/util"; +import { requireValue, validateParam } from "./Validators"; interface IExternalProps { daoAvatarAddress: string; @@ -37,15 +35,12 @@ interface IStateProps { tags: Array abiData: Array abiMethods: Array - abiInputs: IABIField[] - callData: string } interface IABIField { id: string, name: string type: string - inputType: string, placeholder: string, } @@ -57,12 +52,17 @@ const mapDispatchToProps = { }; interface IFormValues { - description: string; - title: string; - url: string; - value: number; - method: string, - [key: string]: any; + description: string + title: string + url: string + value: number + abi: any + methods: Array + method: string + params: IABIField[] + values: any + callData: string + [key: string]: any } const defaultValues: IFormValues = Object.freeze({ @@ -71,7 +71,12 @@ const defaultValues: IFormValues = Object.freeze({ url: "", value: 0, tags: [], + abi: [] as any, + methods: [] as any, method: "", + params: [] as any, + values: [] as any, + callData: "", }); interface INoABIProps { @@ -95,7 +100,7 @@ class CreateGenericPlugin extends React.Component { constructor(props: IProps) { super(props); - this.state = { loading: true, tags: [], abiData: [], abiMethods: [], abiInputs: [], callData: "" }; + this.state = { loading: true, tags: [], abiData: [], abiMethods: [] }; this.handleSubmit = this.handleSubmit.bind(this); this.formModalService = CreateFormModalService( @@ -106,10 +111,10 @@ class CreateGenericPlugin extends React.Component { this.currentFormValues = formValues; if (firstTime) { Object.assign(this.state, { - tags: formValues.tags, abiInputs: this.state.abiInputs, callData: this.state.callData, + tags: formValues.tags, }); } - else { this.setState({ tags: formValues.tags, abiInputs: [], callData: "" }); } + else { this.setState({ tags: formValues.tags }); } }, this.props.showNotification); } @@ -129,11 +134,14 @@ class CreateGenericPlugin extends React.Component { if (!await enableWalletProvider({ showNotification: this.props.showNotification })) { return; } const proposalValues = { - ...values, + title: values.title, + description: values.description, + value: values.value, + callData: values.callData, + url: values.url, dao: this.props.daoAvatarAddress, plugin: this.props.pluginState.address, tags: this.state.tags, - callData: this.state.callData, }; setSubmitting(false); @@ -153,39 +161,37 @@ class CreateGenericPlugin extends React.Component { this.setState({ tags }); } - private getEncodedData = (abi: Array, name: string, values: any[]) => { - const encodedData = encodeABI(abi, name, values); - this.setState({ callData: encodedData }); - } - - private abiInputChange = (values: any) => { + private abiInputChange = (setFieldValue: any, values: any) => { const abiValues = []; - for (const abiInput of this.state.abiInputs) { + for (const abiInput of values.params) { abiValues.push({ type: abiInput.type, value: values[abiInput.name] }); } - - this.getEncodedData(this.state.abiMethods, values.method, abiValues); + setFieldValue("callData", encodeABI(values.methods, values.method, abiValues)); } - private onSelectChange = (data: any): void => { - const abiInputs = data.inputs.map((input: any, index: number) => { + private onSelectChange = (values: any, setFieldValue: any, data: any): void => { + const abiInputs = data[0].inputs.map((input: any, index: number) => { return { id: index, name: input.name, type: input.type, - inputType: input.type === "bool" ? "number" : "text", placeholder: `${input.name} (${input.type})`, methodSignature: input.methodSignature, }; }); - this.setState({ abiInputs: abiInputs, callData: "" }); + setFieldValue("callData", ""); + setFieldValue("params", abiInputs); + if (abiInputs.length === 0) { - this.getEncodedData(this.state.abiMethods, data.methodSignature, []); + setFieldValue("callData", encodeABI(values.methods, data[0].methodSignature, [])); } } public render(): RenderOutput { + const { abiData, abiMethods } = this.state; + this.currentFormValues.abi = abiData; + this.currentFormValues.methods = abiMethods; const { handleClose } = this.props; const contractToCall = (this.props.pluginState as IGenericPluginState).pluginParams.contractToCall; @@ -217,45 +223,8 @@ class CreateGenericPlugin extends React.Component { errors.url = "Invalid URL"; } - const requireValue = (name: string) => { - if ((values as any)[name] === "") { - errors[name] = "Required"; - } - }; - - const nonNegative = (name: string) => { - if ((values as any)[name] < 0) { - errors[name] = "Please enter a non-negative value"; - } - }; - - if (this.state.abiInputs) { - for (const abiValue of this.state.abiInputs) { - require(abiValue.name); - switch (abiValue.type) { - case "address": - if (!isAddress(values[abiValue.name])) { - errors[abiValue.name] = i18next.t("Validate Address"); - } - break; - case "bytes": - if (!isHexString(values[abiValue.name])) { - errors[abiValue.name] = i18next.t("Validate HEX"); - } - break; - case "uint256": - if (/^\d+$/.test(values[abiValue.name]) === false) { - errors[abiValue.name] = i18next.t("Validate Digits"); - } - break; - } - } - } - require("title"); require("description"); - requireValue("value"); - nonNegative("value"); require("method"); return errors; @@ -349,26 +318,31 @@ class CreateGenericPlugin extends React.Component { name="value" type="number" className={touched.value && errors.value ? css.error : null} + validate={requireValue} /> - { setFieldValue("method", data.methodSignature); this.onSelectChange(data); }} - name="method" - placeholder="-- Select method --" - errors={errors} - cssForm={css} - touched={touched} - nameOnList="methodSignature" - /> +
+ + { setFieldValue("method", e.target.value); this.onSelectChange(values, setFieldValue, this.state.abiMethods.filter(method => method.methodSignature === e.target.value)); }} + component="select" + name="method"> + + {this.state.abiMethods.map((method, index) => { + return ; + })} + +
{ - this.state.abiInputs.map((abiInput, index) => { + values.params.map((abiInput, index) => { return (
@@ -393,7 +368,7 @@ class CreateGenericPlugin extends React.Component { } -
{this.state.callData}
+
{values.callData}
diff --git a/src/components/Shared/SelectSearch.scss b/src/components/Shared/SelectSearch.scss deleted file mode 100644 index 344a7edc..00000000 --- a/src/components/Shared/SelectSearch.scss +++ /dev/null @@ -1,57 +0,0 @@ -.selectSearchWrapper { - position: relative; - margin: 20px 0px; - cursor: pointer; - .dropdownSelection { - .arrow { - width: 15px; - height: 15px; - background: url("../../assets/images/Icon/sort-order-arrow.svg") no-repeat right; - transform: rotate(90deg); - position: absolute; - right: 20px; - top: 13px; - } - .arrow:hover { - opacity: 0.7; - } - } - - .dropdownOpen { - display: flex; - flex-direction: column; - top: 60px; - position: absolute; - width: 100%; - z-index: 2; - background-color: $white; - box-shadow: 0px 3px 8px $gray-3; - margin-bottom: 40px; - .search { - position: sticky; - width: 95%; - align-self: center; - margin-top: 15px; - } - .elementsWrapper { - display: flex; - flex-direction: column; - max-height: 100px; - overflow-y: auto; - background-color: $white; - padding: 5px; - overflow-y: scroll; - .element { - padding: 10px; - } - .element:hover { - background-color: $border-accent; - cursor: pointer; - } - .noResults { - align-self: center; - color: $gray-1; - } - } - } -} diff --git a/src/components/Shared/SelectSearch.tsx b/src/components/Shared/SelectSearch.tsx deleted file mode 100644 index d76bf09e..00000000 --- a/src/components/Shared/SelectSearch.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from "react"; -import i18next from "i18next"; -import { ErrorMessage, Field } from "formik"; -import * as css from "./SelectSearch.scss"; - -/** - * Generic Select with Search for formik forms. - * - * data - Objects array - * nameOnList - the property name which will be displayd on the select list - * label - select input label name - * name - 'name' attribute in the parent form - * required (true / false) - * value - parent form value of this filed - * onChange - function to trigger on select. Also returns the whole selected element. - * placeholder [optional] - * errors - errors attribute of the parent form - * cssForm - css styles of the parent form - * touched - touched attribute of the parent form - */ - -interface IProps { - data: Array - label: string - name: string - required: boolean - value: string - onChange: any - placeholder?: string - errors: any - cssForm: any - touched: any - nameOnList: string -} - -export const SelectSearch = (props: IProps): React.ReactElement => { - const [toggle, setToggle] = React.useState(false); - const [search, setSearch] = React.useState(""); - const { data, label, required, onChange, name, placeholder, errors, cssForm, touched, nameOnList, value } = props; - - const handleSelect = (element: any) => { - setToggle(false); - setSearch(""); - onChange(element); - }; - - const elements: any = []; - -data?.forEach((element: any, index: number) => { - if (element.name.toLowerCase().includes(search.toLowerCase())){ - // eslint-disable-next-line react/jsx-no-bind - elements.push(
handleSelect(element)}>{element[nameOnList]}
); - } -}); - -return ( -
- {/* eslint-disable-next-line react/jsx-no-bind */} - -); -}; From ae98836c6e82e74580d729b148d5c7ae3efa416a Mon Sep 17 00:00:00 2001 From: Roie Natan Date: Sun, 18 Oct 2020 11:20:24 +0300 Subject: [PATCH 6/6] better validations handeling ; added interfaces and some minor code improvments --- .../Proposal/Create/PluginForms/ABIService.ts | 53 ++++--------------- .../CreateGenericMultiCallProposal.tsx | 25 ++++----- .../CreateUnknownGenericPluginProposal.tsx | 10 ++-- 3 files changed, 25 insertions(+), 63 deletions(-) diff --git a/src/components/Proposal/Create/PluginForms/ABIService.ts b/src/components/Proposal/Create/PluginForms/ABIService.ts index ba7bbb41..0711f3fe 100644 --- a/src/components/Proposal/Create/PluginForms/ABIService.ts +++ b/src/components/Proposal/Create/PluginForms/ABIService.ts @@ -1,9 +1,9 @@ import { AbiItem } from "web3-utils"; -import { Interface, isHexString } from "ethers/utils"; +import { Interface } from "ethers/utils"; import { SortService } from "lib/sortService"; const Web3 = require("web3"); import axios from "axios"; -import { isAddress, targetedNetwork } from "lib/util"; +import { targetedNetwork } from "lib/util"; export interface IAllowedAbiItem extends AbiItem { name: string @@ -74,36 +74,6 @@ export const extractABIMethods = (abi: AbiItem[]): IAbiItemExtended[] => { .sort(({ name: a }, { name: b }) => SortService.evaluateString(a, b, 1)); }; -/** - * Given array of ABI parameters objects, returns true if all values are valid. - * Data example: - * [{ type: address, value: "0x25112235dDA2F775c81f0AA37a2BaeA21B470f65" }] - * @param {array} data - * @returns {boolean} - */ -export const validateABIInputs = (data: Array): boolean => { - for (const input of data) { - switch (true) { - case input.type.includes("address"): - if (!isAddress(input.value)) { - return false; - } - break; - case input.type.includes("byte"): - if (!isHexString(input.value)) { - return false; - } - break; - case input.type.includes("uint"): - if (/^\d+$/.test(input.value) === false) { - return false; - } - break; - } - } - return true; -}; - /** * Given contract address returns it's ABI data. * @param {string} contractAddress @@ -124,22 +94,17 @@ export const getABIByContract = async (contractAddress: string): Promise, name: string, data: any[]): string => { - const interfaceABI = new Interface(abi); - - if (validateABIInputs(data)) { - const values = []; - for (const input of data) { - values.push(input.value); - } +export const encodeABI = (abi: Array, name: string, values: any[]): string => { + try { + const interfaceABI = new Interface(abi); return interfaceABI.functions[name].encode(values); + } catch (error) { + return error.reason; } - - return ""; }; diff --git a/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx b/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx index 97206930..3ec76b24 100644 --- a/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx +++ b/src/components/Proposal/Create/PluginForms/CreateGenericMultiCallProposal.tsx @@ -37,8 +37,10 @@ interface IDispatchProps { showNotification: typeof showNotification; } +type AddContractError = "NOT_VALID_ADDRESS" | "CONTRACT_EXIST" | "ABI_DATA_ERROR" | ""; + interface IAddContractStatus { - error: string + error: AddContractError message: string } @@ -93,11 +95,11 @@ const defaultValues: IFormValues = Object.freeze({ { address: "", value: 0, - abi: [] as any, - methods: [] as any, + abi: [], + methods: [], method: "", - params: [] as any, - values: [] as any, + params: [], + values: [], callData: "", }, ], @@ -196,7 +198,7 @@ class CreateGenericMultiCallProposal extends React.Component { - const addContractStatus = { + const addContractStatus: IAddContractStatus = { error: "", message: "", }; @@ -250,13 +252,8 @@ class CreateGenericMultiCallProposal extends React.Component { - const abiValues = []; - for (const [i, abiInput] of params.entries()) { - abiValues.push({ type: abiInput.type, value: values[i] }); - } - - const encodedData = encodeABI(abi, name, abiValues); + private abiInputChange = (abi: any, values: any, name: string, setFieldValue: any, index: number) => { // params: any, + const encodedData = encodeABI(abi, name, values); setFieldValue(`contracts.${index}.callData`, encodedData); } @@ -480,7 +477,7 @@ class CreateGenericMultiCallProposal extends React.Component { handleBlur(e); this.abiInputChange(values.contracts[index].abi, values.contracts[index].values, values.contracts[index].method, values.contracts[index].params, setFieldValue, index); }} + onBlur={(e: any) => { handleBlur(e); this.abiInputChange(values.contracts[index].abi, values.contracts[index].values, values.contracts[index].method, setFieldValue, index); }} // eslint-disable-next-line react/jsx-no-bind validate={(e: any) => validateParam(param.type, e)} /> diff --git a/src/components/Proposal/Create/PluginForms/CreateUnknownGenericPluginProposal.tsx b/src/components/Proposal/Create/PluginForms/CreateUnknownGenericPluginProposal.tsx index 9d91d953..bdc9c36e 100644 --- a/src/components/Proposal/Create/PluginForms/CreateUnknownGenericPluginProposal.tsx +++ b/src/components/Proposal/Create/PluginForms/CreateUnknownGenericPluginProposal.tsx @@ -71,11 +71,11 @@ const defaultValues: IFormValues = Object.freeze({ url: "", value: 0, tags: [], - abi: [] as any, - methods: [] as any, + abi: [], + methods: [], method: "", - params: [] as any, - values: [] as any, + params: [], + values: [], callData: "", }); @@ -164,7 +164,7 @@ class CreateGenericPlugin extends React.Component { private abiInputChange = (setFieldValue: any, values: any) => { const abiValues = []; for (const abiInput of values.params) { - abiValues.push({ type: abiInput.type, value: values[abiInput.name] }); + abiValues.push(values[abiInput.name]); } setFieldValue("callData", encodeABI(values.methods, values.method, abiValues)); }