diff --git a/package-lock.json b/package-lock.json index e0513dd0e..832315830 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2251,12 +2251,41 @@ "is-buffer": "^2.0.2" } }, + "bignumber.js": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", + "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, "camelcase": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "dev": true }, + "cbor": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-4.3.0.tgz", + "integrity": "sha512-CvzaxQlaJVa88sdtTWvLJ++MbdtPHtZOBBNjm7h3YKUHILMs9nQyD4AC6hvFZy7GBVB3I6bRibJcxeHydyT2IQ==", + "requires": { + "bignumber.js": "^9.0.0", + "commander": "^3.0.0", + "json-text-sequence": "^0.1", + "nofilter": "^1.0.3" + }, + "dependencies": { + "bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" + } + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -2279,12 +2308,28 @@ "wrap-ansi": "^2.0.0" } }, + "commander": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==" + }, "dotenv": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==", "dev": true }, + "eth-lib": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.7.tgz", + "integrity": "sha1-L5Pxex4jrsN1nNSj/iDBKGo/wco=", + "dev": true, + "requires": { + "bn.js": "^4.11.6", + "elliptic": "^6.4.0", + "xhr-request-promise": "^0.1.2" + } + }, "fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -2377,6 +2422,46 @@ "yargs-parser": "10.x" } }, + "web3": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.2.6.tgz", + "integrity": "sha512-tpu9fLIComgxGrFsD8LUtA4s4aCZk7px8UfcdEy6kS2uDi/ZfR07KJqpXZMij7Jvlq+cQrTAhsPSiBVvoMaivA==", + "dev": true, + "requires": { + "@types/node": "^12.6.1", + "web3-bzz": "1.2.6", + "web3-core": "1.2.6", + "web3-eth": "1.2.6", + "web3-eth-personal": "1.2.6", + "web3-net": "1.2.6", + "web3-shh": "1.2.6", + "web3-utils": "1.2.6" + }, + "dependencies": { + "@types/node": { + "version": "12.12.54", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.54.tgz", + "integrity": "sha512-ge4xZ3vSBornVYlDnk7yZ0gK6ChHf/CHB7Gl1I0Jhah8DDnEQqBzgohYG4FX4p81TNirSETOiSyn+y1r9/IR6w==", + "dev": true + } + } + }, + "web3-utils": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.2.6.tgz", + "integrity": "sha512-8/HnqG/l7dGmKMgEL9JeKPTtjScxOePTzopv5aaKFExPfaBrYRkgoMqhoowCiAl/s16QaTn4DoIF1QC4YsT7Mg==", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "eth-lib": "0.2.7", + "ethereum-bloom-filters": "^1.0.6", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "underscore": "1.9.1", + "utf8": "3.0.0" + } + }, "wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", @@ -9444,6 +9529,35 @@ "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", "dev": true }, + "webdriverio": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-6.4.7.tgz", + "integrity": "sha512-+wwmmiVFPb4PEh9bGfBIdK4zKcT3NYPhQ9LfddnUIit5Qah0CjL3iY+UCY28IVp5nzZd9XPZNr71W4bErbcDQg==", + "dev": true, + "requires": { + "@types/puppeteer": "^3.0.1", + "@wdio/config": "6.4.7", + "@wdio/logger": "6.4.7", + "@wdio/repl": "6.4.7", + "@wdio/utils": "6.4.7", + "archiver": "^5.0.0", + "atob": "^2.1.2", + "css-value": "^0.0.1", + "devtools": "6.4.7", + "get-port": "^5.1.1", + "grapheme-splitter": "^1.0.2", + "lodash.clonedeep": "^4.5.0", + "lodash.isobject": "^3.0.2", + "lodash.isplainobject": "^4.0.6", + "lodash.zip": "^4.2.0", + "minimatch": "^3.0.4", + "puppeteer-core": "^5.1.0", + "resq": "^1.6.0", + "rgb2hex": "^0.2.0", + "serialize-error": "^7.0.0", + "webdriver": "6.4.7" + } + }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -9733,6 +9847,35 @@ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true + }, + "webdriverio": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-6.4.7.tgz", + "integrity": "sha512-+wwmmiVFPb4PEh9bGfBIdK4zKcT3NYPhQ9LfddnUIit5Qah0CjL3iY+UCY28IVp5nzZd9XPZNr71W4bErbcDQg==", + "dev": true, + "requires": { + "@types/puppeteer": "^3.0.1", + "@wdio/config": "6.4.7", + "@wdio/logger": "6.4.7", + "@wdio/repl": "6.4.7", + "@wdio/utils": "6.4.7", + "archiver": "^5.0.0", + "atob": "^2.1.2", + "css-value": "^0.0.1", + "devtools": "6.4.7", + "get-port": "^5.1.1", + "grapheme-splitter": "^1.0.2", + "lodash.clonedeep": "^4.5.0", + "lodash.isobject": "^3.0.2", + "lodash.isplainobject": "^4.0.6", + "lodash.zip": "^4.2.0", + "minimatch": "^3.0.4", + "puppeteer-core": "^5.1.0", + "resq": "^1.6.0", + "rgb2hex": "^0.2.0", + "serialize-error": "^7.0.0", + "webdriver": "6.4.7" + } } } }, @@ -16558,6 +16701,23 @@ "uuid": "^8.0.0" }, "dependencies": { + "@wdio/config": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-6.4.7.tgz", + "integrity": "sha512-wtcj9yKm5+SivwhsgpusBrFR7a3rpDsN/WH6ekoqlZFs7oCpJeTLwawWnoX6MJQy2no5o00lGxDDJnqjaBdiiQ==", + "dev": true, + "requires": { + "@wdio/logger": "6.4.7", + "deepmerge": "^4.0.0", + "glob": "^7.1.2" + } + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, "uuid": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", diff --git a/src/actions/arcActions.ts b/src/actions/arcActions.ts index 9d9bcadc2..19a45690e 100644 --- a/src/actions/arcActions.ts +++ b/src/actions/arcActions.ts @@ -8,8 +8,9 @@ import { ReputationFromTokenPlugin, Proposal, FundingRequestProposal, - JoinProposal, IProposalState, + TokenTradeProposal, + JoinProposal, } from "@daostack/arc.js"; import { IAsyncAction } from "actions/async"; import { getArc } from "arc"; @@ -91,6 +92,9 @@ async function tryRedeemProposal(proposalId: string, accountAddress: string, obs case "Join": await (proposal as JoinProposal).redeem().subscribe(...observer); break; + case "TokenTrade": + await (proposal as TokenTradeProposal).redeem().subscribe(...observer); + break; default: break; } diff --git a/src/components/Proposal/Create/CreateProposalPage.tsx b/src/components/Proposal/Create/CreateProposalPage.tsx index 4d41d05c6..c6e38029f 100644 --- a/src/components/Proposal/Create/CreateProposalPage.tsx +++ b/src/components/Proposal/Create/CreateProposalPage.tsx @@ -20,6 +20,8 @@ import { pluginName } from "lib/pluginUtils"; import * as css from "./CreateProposal.scss"; import CreatePluginManagerProposal from "./PluginForms/CreatePluginManagerProposal"; import { of } from "rxjs"; +import CreateTokenTradeProposal from "./PluginForms/CreateTokenTradeProposal"; +import { ITokenTradeState } from "@daostack/arc.js"; type IExternalProps = RouteComponentProps; @@ -120,6 +122,8 @@ class CreateProposalPage extends React.Component { createPluginComponent = ; } else if (pluginState.name === "SchemeFactory") { createPluginComponent = ; + } else if (pluginState.name === "TokenTrade") { + createPluginComponent = ; } else if (pluginState.name === "GenericScheme") { const contractToCall = (pluginState as IGenericPluginState).pluginParams.contractToCall; if (!contractToCall) { diff --git a/src/components/Proposal/Create/PluginForms/CreatePluginManagerProposal.tsx b/src/components/Proposal/Create/PluginForms/CreatePluginManagerProposal.tsx index 44fc5e1bf..9dbc10359 100644 --- a/src/components/Proposal/Create/PluginForms/CreatePluginManagerProposal.tsx +++ b/src/components/Proposal/Create/PluginForms/CreatePluginManagerProposal.tsx @@ -125,6 +125,10 @@ export interface IFormValues { fundingGoalDeadline: number; rageQuitEnable: boolean; }; + TokenTrade: { + permissions: IPermissions; + votingParams: IGenesisProtocolFormValues; + } SchemeRegistrar: { permissions: IPermissions; votingParamsRegister: IGenesisProtocolFormValues; @@ -189,6 +193,15 @@ const defaultValues: IFormValues = { genericCall: false, }, }, + TokenTrade: { + votingParams: { ...votingParams }, + permissions: { + registerPlugins: false, + changeConstraints: false, + upgradeController: false, + genericCall: false, + }, + }, Competition: { votingParams: { ...votingParams }, permissions: { @@ -392,6 +405,15 @@ class CreatePluginManagerProposal extends React.Component { daoFactory: arc.getContractInfoByName("DAOFactoryInstance", LATEST_ARC_VERSION).address, }; break; + case "TokenTrade": + proposalOptions.add.pluginInitParams = { + daoId: daoId, + votingMachine: votingMachine, + votingParams: gpFormValuesToVotingParams(values.ContributionReward.votingParams), + voteOnBehalf: values.ContributionReward.votingParams.voteOnBehalf, + voteParamsHash: values.ContributionReward.votingParams.voteParamsHash, + }; + break; case "SchemeRegistrar": proposalOptions.add.pluginInitParams = { daoId: daoId, diff --git a/src/components/Proposal/Create/PluginForms/CreateTokenTradeProposal.tsx b/src/components/Proposal/Create/PluginForms/CreateTokenTradeProposal.tsx new file mode 100644 index 000000000..ad682ee07 --- /dev/null +++ b/src/components/Proposal/Create/PluginForms/CreateTokenTradeProposal.tsx @@ -0,0 +1,360 @@ + +import * as React from "react"; +import { connect } from "react-redux"; +import { IPluginState } from "@daostack/arc.js"; +import { enableWalletProvider } from "arc"; + +import { ErrorMessage, Field, Form, Formik, FormikProps } from "formik"; + +import { NotificationStatus, showNotification } from "reducers/notifications"; +import * as arcActions from "actions/arcActions"; + +import Analytics from "lib/analytics"; +import { isValidUrl, supportedTokens } from "lib/util"; +import { exportUrl } from "lib/proposalUtils"; + +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 { IRootState } from "reducers"; + +interface IExternalProps { + daoAvatarAddress: string; + handleClose: () => any; + pluginState: IPluginState; +} + +interface IDispatchProps { + createProposal: typeof arcActions.createProposal; + showNotification: typeof showNotification; +} + +const mapStateToProps = (state: IRootState, ownProps: IExternalProps) => { + return ownProps; +}; + +const mapDispatchToProps = { + createProposal: arcActions.createProposal, + showNotification, +}; + +type IProps = IExternalProps & IDispatchProps; + +interface IFormValues { + description: string; + title: string; + url: string; + tags: Array; + sendTokenAddress: string, + sendTokenAmount: number, + receiveTokenAddress: string, + receiveTokenAmount: number, +} + +interface IState { + tags: Array; + tokens: { + value: string; + label: string; + }[] +} + +const setInitialFormValues = (): IFormValues => { + return Object.freeze({ + description: "", + title: "", + url: "", + tags: [], + sendTokenAddress: "", + sendTokenAmount: 0, + receiveTokenAddress: "", + receiveTokenAmount: 0, + }); +}; + +class CreateTokenTradeProposal extends React.Component { + + formModalService: IFormModalService; + currentFormValues: IFormValues; + + constructor(props: IProps) { + super(props); + + const tokens = Object.keys(supportedTokens()).map((tokenAddress) => { + const token = supportedTokens()[tokenAddress]; + return { value: tokenAddress, label: token["symbol"] }; + }); + + this.state = { tags: [], tokens: [] }; + + this.formModalService = CreateFormModalService( + "CreateTokenTradeProposal", + setInitialFormValues(), + () => Object.assign(this.currentFormValues, this.state), + (formValues: IFormValues, firstTime: boolean) => { + this.currentFormValues = formValues; + if (firstTime) { this.state = { tags: formValues.tags, tokens }; } + else { this.setState({ tags: formValues.tags, tokens }); } + }, + this.props.showNotification); + } + + componentWillUnmount() { + this.formModalService.saveCurrentValues(); + } + + private handleSubmit = async (values: IFormValues, { setSubmitting }: any ): Promise => { + if (!await enableWalletProvider({ showNotification: this.props.showNotification })) { return; } + + const proposalOptions = { + dao: this.props.daoAvatarAddress, + description: values.description, + title: values.title, + tags: this.state.tags, + plugin: this.props.pluginState.address, + url: values.url, + sendTokenAddress: values.sendTokenAddress, + sendTokenAmount: values.sendTokenAmount, + receiveTokenAddress: values.receiveTokenAddress, + receiveTokenAmount: values.receiveTokenAmount, + }; + + setSubmitting(false); + + await this.props.createProposal(proposalOptions); + + 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}); + } + + public exportFormValues(values: IFormValues) { + values = { + ...values, + ...this.state, + }; + exportUrl(values); + this.props.showNotification(NotificationStatus.Success, "Exportable url is now in clipboard :)"); + } + + public render(): RenderOutput { + const { handleClose } = this.props; + + return ( +
+ + { + + this.currentFormValues = values; + + const errors: any = {}; + + const require = (name: string) => { + if (!(values as any)[name]) { + errors[name] = "Required"; + } + }; + + require("description"); + require("title"); + require("sendTokenAddress"); + require("sendTokenAmount"); + require("receiveTokenAddress"); + require("receiveTokenAmount"); + + if (values.title.length > 120) { + errors.title = "Title is too long (max 120 characters)"; + } + + if (!isValidUrl(values.url)) { + errors.url = "Invalid URL"; + } + + return errors; + }} + onSubmit={this.handleSubmit} + // eslint-disable-next-line react/jsx-no-bind + render={({ + errors, + touched, + isSubmitting, + setFieldValue, + values, + }: FormikProps) => { + return ( +
+ +
Propose to trade tokens with the DAO.
+ + + + + + + + + setFieldValue("description", value)} + id="descriptionInput" + placeholder={i18next.t("Description Placeholder")} + name="description" + className={touched.description && errors.description ? css.error : null} + /> + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+
+ + + {Object.keys(supportedTokens()).map((tokenAddress, _i) => { + const token = supportedTokens()[tokenAddress]; + return +
+
+
+ +
+ +
+
+ +
+
+ + + { + this.state.tokens.map((token, _i) => { + return +
+
+
+ +
+ + { /* eslint-disable-next-line react/jsx-no-bind */ } + + + + + + +
+ + ); + }} + /> +
+ ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(CreateTokenTradeProposal); diff --git a/src/components/Proposal/Create/PluginForms/PluginInitializeFields.tsx b/src/components/Proposal/Create/PluginForms/PluginInitializeFields.tsx index 7444f898c..9a2993ed0 100644 --- a/src/components/Proposal/Create/PluginForms/PluginInitializeFields.tsx +++ b/src/components/Proposal/Create/PluginForms/PluginInitializeFields.tsx @@ -141,6 +141,12 @@ const Join = () => ( ); +const TokenTrade = () => ( +
+ {GenesisProtocolFields("TokenTrade.votingParams")} +
+); + const SchemeRegistrarFields = () => (
@@ -174,6 +180,7 @@ const fieldsMap = { ContributionRewardExt: ContributionRewardExtFields, FundingRequest: FundingRequest, Join: Join, + TokenTrade: TokenTrade, SchemeRegistrar: SchemeRegistrarFields, SchemeFactory: PluginManagerFields, ReputationFromToken: ReputationFromTokenFields, diff --git a/src/components/Proposal/ProposalSummary/ProposalSummary.scss b/src/components/Proposal/ProposalSummary/ProposalSummary.scss index a67d45789..e032eec7d 100644 --- a/src/components/Proposal/ProposalSummary/ProposalSummary.scss +++ b/src/components/Proposal/ProposalSummary/ProposalSummary.scss @@ -198,6 +198,10 @@ margin-right: 7px; } +.bold { + font-weight: bold; +} + .detailView { text-align: left; diff --git a/src/components/Proposal/ProposalSummary/ProposalSummary.tsx b/src/components/Proposal/ProposalSummary/ProposalSummary.tsx index 1a1eaf056..aba594d5d 100644 --- a/src/components/Proposal/ProposalSummary/ProposalSummary.tsx +++ b/src/components/Proposal/ProposalSummary/ProposalSummary.tsx @@ -8,7 +8,9 @@ import { Proposal, IPluginManagerProposalState, IFundingRequestProposalState, - IJoinProposalState } from "@daostack/arc.js"; + ITokenTradeProposalState, + IJoinProposalState, +} from "@daostack/arc.js"; import classNames from "classnames"; import { GenericPluginRegistry } from "genericPluginRegistry"; import * as React from "react"; @@ -22,6 +24,7 @@ import ProposalSummaryUnknownGenericPlugin from "./ProposalSummaryUnknownGeneric import ProposalSummaryJoin from "./ProposalSummaryJoin"; import ProposalSummaryFundingRequest from "./ProposalSummaryFundingRequest"; import { getArc } from "arc"; +import ProposalSummaryTokenTrade from "./ProposalSummaryTokenTrade"; interface IProps { beneficiaryProfile?: IProfileState; @@ -64,6 +67,9 @@ export default class ProposalSummary extends React.Component<IProps, IState> { } else if (proposal.coreState.name.includes("SchemeRegistrar")) { const state = proposal.coreState as IPluginRegistrarProposalState; return <ProposalSummaryPluginRegistrar {...this.props} proposalState={state} />; + } else if (proposal.coreState.name.includes("TokenTrade")) { + const state = proposal.coreState as ITokenTradeProposalState; + return <ProposalSummaryTokenTrade {...this.props} proposalState={state} />; } else if (proposal.coreState.name.includes("SchemeFactory")) { const state = proposal.coreState as IPluginManagerProposalState; return <ProposalSummaryPluginManager {...this.props} proposalState={state} />; diff --git a/src/components/Proposal/ProposalSummary/ProposalSummaryTokenTrade.tsx b/src/components/Proposal/ProposalSummary/ProposalSummaryTokenTrade.tsx new file mode 100644 index 000000000..56615b702 --- /dev/null +++ b/src/components/Proposal/ProposalSummary/ProposalSummaryTokenTrade.tsx @@ -0,0 +1,84 @@ +import { IDAOState, ITokenTradeProposalState } from "@daostack/arc.js"; +import classNames from "classnames"; +import * as React from "react"; +import AccountPopup from "components/Account/AccountPopup"; +import AccountProfileName from "components/Account/AccountProfileName"; +import { IProfileState } from "reducers/profilesReducer"; + +import * as css from "./ProposalSummary.scss"; +import { tokenDetails, formatTokens, toWei } from "lib/util"; +import i18next from "i18next"; + +interface IProps { + beneficiaryProfile?: IProfileState; + detailView?: boolean; + daoState: IDAOState; + proposalState: ITokenTradeProposalState; + transactionModal?: boolean; +} + +export default class ProposalSummaryTokenTrade extends React.Component<IProps> { + + constructor(props: IProps) { + super(props); + } + + public render(): RenderOutput { + + const { beneficiaryProfile, proposalState, daoState, detailView, transactionModal } = this.props; + + let receiveToken; + let sendToken; + + if (proposalState.sendTokenAddress && proposalState.sendTokenAmount) { + const tokenData = tokenDetails(proposalState.sendTokenAddress); + sendToken = formatTokens(toWei(Number(proposalState.sendTokenAmount)), tokenData ? tokenData["symbol"] : "?", tokenData ? tokenData["decimals"] : 18); + } + + if (proposalState.receiveTokenAddress && proposalState.receiveTokenAmount) { + const tokenData = tokenDetails(proposalState.receiveTokenAddress); + receiveToken = formatTokens(toWei(Number(proposalState.receiveTokenAmount)), tokenData ? tokenData["symbol"] : "?", tokenData ? tokenData["decimals"] : 18); + } + + const proposalSummaryClass = classNames({ + [css.detailView]: detailView, + [css.transactionModal]: transactionModal, + [css.proposalSummary]: true, + }); + return ( + <div className={proposalSummaryClass}> + <span className={css.transferType}> + { sendToken && + <div> + <div> + <span className={css.bold}>{i18next.t("Send to DAO")}:</span> + </div> + <AccountPopup accountAddress={proposalState.beneficiary} daoState={daoState} width={12} /> + <span> + <AccountProfileName accountAddress={proposalState.beneficiary} accountProfile={beneficiaryProfile} daoAvatarAddress={daoState.address}/> + </span> + <span className={css.transferAmount}></span> + <img className={css.transferIcon} src="/assets/images/Icon/Transfer.svg" /> + {receiveToken} + </div> + } + { receiveToken && + <div> + <div> + <span className={css.bold}>{i18next.t("Receive from DAO")}:</span> + </div> + {receiveToken} + <span className={css.transferAmount}></span> + <img className={css.transferIcon} src="/assets/images/Icon/Transfer.svg" /> + <AccountPopup accountAddress={proposalState.beneficiary} daoState={daoState} width={12} /> + <span> + <AccountProfileName accountAddress={proposalState.beneficiary} accountProfile={beneficiaryProfile} daoAvatarAddress={daoState.address}/> + </span> + </div> + } + </span> + </div> + ); + + } +} diff --git a/src/lib/pluginUtils.ts b/src/lib/pluginUtils.ts index d94ce2856..900d96dc8 100644 --- a/src/lib/pluginUtils.ts +++ b/src/lib/pluginUtils.ts @@ -44,6 +44,7 @@ export const REQUIRED_PLUGIN_PERMISSIONS: any = { "VoteInOrganizationScheme": PluginPermissions.IsRegistered | PluginPermissions.CanCallDelegateCall, "Join": PluginPermissions.IsRegistered, "FundingRequest": PluginPermissions.IsRegistered, + "TokenTrade": PluginPermissions.IsRegistered, }; /** plugins that we know how to interpret */ @@ -54,6 +55,7 @@ export const PLUGIN_NAMES = { SchemeRegistrar: "Plugin Registrar", SchemeFactory: "Plugin Manager", Competition: "Competition", + TokenTrade: "Token Trade", ContributionRewardExt: "Contribution Reward Ext", Join: "Join", FundingRequest: "Funding Request", diff --git a/test/integration/proposal-tokenTrade.ts b/test/integration/proposal-tokenTrade.ts new file mode 100644 index 000000000..7e28b2952 --- /dev/null +++ b/test/integration/proposal-tokenTrade.ts @@ -0,0 +1,65 @@ +import * as uuid from "uuid"; +import { LATEST_ARC_VERSION, hideCookieAcceptWindow, ITestAddresses, gotoDaoPlugins } from "./utils"; + +describe("Token Trade Proposals", () => { + let daoAddress: string; + let addresses: ITestAddresses; + + before(() => { + const { daos } = require("@daostack/test-env-experimental/daos.json"); + addresses = daos[LATEST_ARC_VERSION].find((dao: any) => dao.name === "DAO For Testing"); + daoAddress = addresses.Avatar.toLowerCase(); + }); + + it("Create a proposal to trade some tokens", async () => { + await gotoDaoPlugins(daoAddress); + + const pluginCard = await $("[data-test-id=\"pluginCard-TokenTrade\"]"); + await pluginCard.waitForExist(); + await pluginCard.click(); + + await hideCookieAcceptWindow(); + + const createProposalButton = await $("a[data-test-id=\"createProposal\"]"); + await createProposalButton.waitForExist(); + await createProposalButton.click(); + + const titleInput = await $("*[id=\"titleInput\"]"); + await titleInput.waitForExist(); + + const title = uuid(); + await titleInput.setValue(title); + + const descriptionInput = await $(".mde-text"); + await descriptionInput.setValue("Trade some tokens"); + + const urlInput = await $("*[id=\"urlInput\"]"); + await urlInput.setValue(`https://this.must.be/a/valid/url${uuid()}/lets.trade.tokens`); + + const sendTokenAmountInput = await $("*[id=\"sendTokenAmountInput\"]"); + await sendTokenAmountInput.scrollIntoView(); + await sendTokenAmountInput.setValue("10"); + + const sendTokenAddressInput = await $("select[id=\"sendTokenAddress\"]"); + await sendTokenAddressInput.scrollIntoView(); + await sendTokenAddressInput.selectByIndex(1); + + const receiveTokenAmountInput = await $("*[id=\"receiveTokenAmountInput\"]"); + await receiveTokenAmountInput.scrollIntoView(); + await receiveTokenAmountInput.setValue("10"); + + const receiveTokenAddressInput = await $("select[id=\"receiveTokenAddress\"]"); + await receiveTokenAddressInput.scrollIntoView(); + await receiveTokenAddressInput.selectByIndex(1); + + const createProposalSubmitButton = await $("*[type=\"submit\"]"); + await createProposalSubmitButton.scrollIntoView(); + await createProposalSubmitButton.click(); + + // check that the proposal appears in the list + // test for the title + const titleElement = await $(`[data-test-id="proposal-title"]=${title}`); + await titleElement.waitForExist(); + }); + +});