diff --git a/.travis.yml b/.travis.yml index 26ce5cea1..cd2550533 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: node_js -node_js: 12.16.2 +node_js: 12.18.3 addons: apt: update: true @@ -14,49 +14,55 @@ before_install: addons: chrome: stable -script: - - docker -v - - docker-compose -v - # fail on error - # - set -e - - echo 'docker-compose build' && echo -en 'travis_fold:start:script.1\\r' - - docker-compose build +jobs: + include: + - stage: tests + name: "Unit tests" + script: + - docker -v + - docker-compose -v + # fail on error + # - set -e + - echo 'docker-compose build' && echo -en 'travis_fold:start:script.1\\r' + - docker-compose build - - echo -en 'travis_fold:end:script.1\\r' - - echo 'docker-compose up -d' && echo -en 'travis_fold:start:script.2\\r' - - docker-compose up -d - - echo -en 'travis_fold:end:script.2\\r' + - echo -en 'travis_fold:end:script.1\\r' + - echo 'docker-compose up -d' && echo -en 'travis_fold:start:script.2\\r' + - docker-compose up -d + - echo -en 'travis_fold:end:script.2\\r' - # wait for alchemy (the slowest latest process to respond - - bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' 127.0.0.1:3000)" != "200" ]]; do sleep 5; done' - # prin the status of the services - - npm run service-status + # wait for alchemy (the slowest latest process to respond + - bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' 127.0.0.1:3000)" != "200" ]]; do sleep 5; done' + # prin the status of the services + - npm run service-status - # show the daos that are indexed - - ./scripts/checkDaos.sh - - echo -en 'travis_fold:end:script.4\\r' + # show the daos that are indexed + - ./scripts/checkDaos.sh + - echo -en 'travis_fold:end:script.4\\r' - # check lint - - npm run lint - # unit tests - - npm run test:unit -- --forceExit + # unit tests + - npm run test:unit -- --forceExit + # run coverage report + - docker-compose logs alchemy || true + # run integration tests + - npm run test:integration:headless + - npm run report-coverage + # get some diagnostic info fo debugging travis + - echo 'Debug info:' && echo -en 'travis_fold:start:script.3\\r' + - npm run service-status + - docker-compose logs alchemy || true + - echo -en 'travis_fold:end:script.3\\r' - - docker-compose logs alchemy || true - # run integration tests - - npm run test:integration:headless + - ./scripts/checkDaos.sh - # get some diagnostic info fo debugging travis - - echo 'Debug info:' && echo -en 'travis_fold:start:script.3\\r' - - npm run service-status - - docker-compose logs alchemy || true - - echo -en 'travis_fold:end:script.3\\r' - - - ./scripts/checkDaos.sh - - # see if the app builds correctly - - echo 'npm run build-travis' && echo -en 'travis_fold:start:script.4\\r' - - npm run build-travis - - echo -en 'travis_fold:end:script.4\\r' + # see if the app builds correctly + - echo 'npm run build-travis' && echo -en 'travis_fold:start:script.4\\r' + - npm run build-travis + - echo -en 'travis_fold:end:script.4\\r' + + - stage: tests + name: "ts/es lint" + script: npm run lint deploy: provider: pages diff --git a/package-lock.json b/package-lock.json index e0513dd0e..d5a06fa81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1973,9 +1973,9 @@ } }, "@daostack/arc.js": { - "version": "2.0.0-experimental.55", - "resolved": "https://registry.npmjs.org/@daostack/arc.js/-/arc.js-2.0.0-experimental.55.tgz", - "integrity": "sha512-L1h+W6LyI6qmhGGgcdTMn7X86Qp90WksxhWiERTwxC7G4bihussgCC4lHonHS3X22Hfp2t67/vY4osCC1r+RhA==", + "version": "2.0.0-experimental.56", + "resolved": "https://registry.npmjs.org/@daostack/arc.js/-/arc.js-2.0.0-experimental.56.tgz", + "integrity": "sha512-mBXyogidf+q2jvrBgfvAC+jT/0gmA6a8AbFD+rNm/GLvwOShoqfy5BBOaVbPJ1eevfeiC7Gc3fAMyXdKx5I7hw==", "requires": { "abi-decoder": "^2.3.0", "apollo-cache-inmemory": "^1.6.5", @@ -4039,40 +4039,11 @@ "@ethersproject/strings": "^5.0.3" } }, - "@fortawesome/fontawesome-common-types": { - "version": "0.2.30", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.30.tgz", - "integrity": "sha512-TsRwpTuKwFNiPhk1UfKgw7zNPeV5RhNp2Uw3pws+9gDAkPGKrtjR1y2lI3SYn7+YzyfuNknflpBA1LRKjt7hMg==" - }, "@fortawesome/fontawesome-free": { "version": "5.14.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.14.0.tgz", "integrity": "sha512-OfdMsF+ZQgdKHP9jUbmDcRrP0eX90XXrsXIdyjLbkmSBzmMXPABB8eobUJtivaupucYaByz6WNe1PI1JuYm3qA==" }, - "@fortawesome/fontawesome-svg-core": { - "version": "1.2.30", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.30.tgz", - "integrity": "sha512-E3sAXATKCSVnT17HYmZjjbcmwihrNOCkoU7dVMlasrcwiJAHxSKeZ+4WN5O+ElgO/FaYgJmASl8p9N7/B/RttA==", - "requires": { - "@fortawesome/fontawesome-common-types": "^0.2.30" - } - }, - "@fortawesome/free-brands-svg-icons": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.14.0.tgz", - "integrity": "sha512-WsqPFTvJFI7MYkcy0jeFE2zY+blC4OrnB9MJOcn1NxRXT/sSfEEhrI7CwzIkiYajLiVDBKWeErYOvpsMeodmCQ==", - "requires": { - "@fortawesome/fontawesome-common-types": "^0.2.30" - } - }, - "@fortawesome/react-fontawesome": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.11.tgz", - "integrity": "sha512-sClfojasRifQKI0OPqTy8Ln8iIhnxR/Pv/hukBhWnBz9kQRmqi6JSH3nghlhAY7SUeIIM7B5/D2G8WjX0iepVg==", - "requires": { - "prop-types": "^15.7.2" - } - }, "@graphprotocol/graph-cli": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/@graphprotocol/graph-cli/-/graph-cli-0.18.0.tgz", @@ -15142,6 +15113,19 @@ "yaml": "^1.7.2" } }, + "coveralls": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.1.0.tgz", + "integrity": "sha512-sHxOu2ELzW8/NC1UP5XVLbZDzO4S3VxfFye3XYCznopHy02YjNkHcj5bKaVw2O7hVaBdBjEdQGpie4II1mWhuQ==", + "dev": true, + "requires": { + "js-yaml": "^3.13.1", + "lcov-parse": "^1.0.0", + "log-driver": "^1.2.7", + "minimist": "^1.2.5", + "request": "^2.88.2" + } + }, "crc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", @@ -28799,6 +28783,12 @@ "invert-kv": "^2.0.0" } }, + "lcov-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", + "integrity": "sha1-6w1GtUER68VhrLTECO+TY73I9+A=", + "dev": true + }, "left-pad": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", @@ -31087,6 +31077,12 @@ "integrity": "sha1-7GZi5IlkCO1KtsVCo5kLcswIACA=", "dev": true }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true + }, "log-ok": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/log-ok/-/log-ok-0.1.1.tgz", diff --git a/package.json b/package.json index 4bf2b5894..1aac3f4fb 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,22 @@ "json", "node" ], + "coveragePathIgnorePatterns": [ + "/node_modules/", + "/test/" + ], + "coverageThreshold": { + "global": { + "branches": 8, + "functions": 8, + "lines": 8, + "statements": 8 + } + }, + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{js,ts}" + ], "moduleNameMapper": { "\\.(scss|css|less|svg|png|jpg)$": "identity-obj-proxy", "^arc": "/src/arc", @@ -71,6 +87,7 @@ "start-staging": "npm run start-staging-rinkeby", "start-xdai": "cross-env NODE_ENV=production SHOW_ALL_DAOS=true NETWORK=xdai node --max_old_space_size=4096 ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --config webpack.dev.config.js", "storybook": "start-storybook", + "report-coverage": "cat ./coverage/lcov.info | coveralls", "test": "wdio ./test/integration/wdio.conf.js", "test:integration": "wdio ./test/integration/wdio.conf.js --inspect", "test:integration:headless": "wdio ./test/integration/wdio-headless.conf.js", @@ -79,7 +96,7 @@ "dependencies": { "3box": "^1.20.3", "@burner-wallet/burner-connect-provider": "^0.1.1", - "@daostack/arc.js": "2.0.0-experimental.55", + "@daostack/arc.js": "2.0.0-experimental.56", "@dorgtech/daocreator-ui-experimental": "1.1.13", "@portis/web3": "^2.0.0-beta.56", "@sentry/browser": "^5.0.8", @@ -195,6 +212,7 @@ "babel-loader": "^8.0.6", "chai": "^4.2.0", "copy-webpack-plugin": "^5.1.1", + "coveralls": "^3.1.0", "cross-env": "^5.1.3", "dotenv": "^8.2.0", "eslint": "^7.3.1", 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/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index 21d53cfc0..9c998655e 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -44,5 +44,7 @@ "Reputation Minted": "Reputation Minted:", "Minimum DAO bounty": "Minimum DAO bounty:", "Amount": "Amount:", - "Amount Redeemed": "Amount Redeemed:" + "Amount Redeemed": "Amount Redeemed:", + "Add Params": "Add Params", + "Remove Params": "Remove Params" } diff --git a/src/components/Account/AccountBalances.tsx b/src/components/Account/AccountBalances.tsx index 2e993ba51..40436e62e 100644 --- a/src/components/Account/AccountBalances.tsx +++ b/src/components/Account/AccountBalances.tsx @@ -8,6 +8,7 @@ import * as css from "layouts/App.scss"; import * as React from "react"; import { combineLatest, of } from "rxjs"; import { getArc } from "arc"; +import { GRAPH_POLL_INTERVAL } from "../../settings"; interface IExternalProps { daoState?: IDAOState; @@ -76,7 +77,7 @@ export default withSubscription({ })) : undefined; return combineLatest( - member ? member.state( { subscribe: true }).pipe(ethErrorHandler()) : of(null), + member ? member.state( { polling: true, pollInterval: GRAPH_POLL_INTERVAL }).pipe(ethErrorHandler()) : of(null), ethBalance(accountAddress).pipe(ethErrorHandler()), arc.GENToken().balanceOf(accountAddress).pipe(ethErrorHandler()) ); diff --git a/src/components/Account/AccountProfilePage.tsx b/src/components/Account/AccountProfilePage.tsx index 6fc2653be..9d212d87f 100644 --- a/src/components/Account/AccountProfilePage.tsx +++ b/src/components/Account/AccountProfilePage.tsx @@ -25,6 +25,7 @@ import { IProfileState } from "reducers/profilesReducer"; import { combineLatest, of } from "rxjs"; import Loading from "components/Shared/Loading"; import * as css from "./Account.scss"; +import { GRAPH_POLL_INTERVAL } from "../../settings"; type IExternalProps = RouteComponentProps; @@ -355,7 +356,7 @@ const SubscribedAccountProfilePage = withSubscription({ return combineLatest( // subscribe if only to to get DAO reputation supply updates - daoAvatarAddress ? dao.state({subscribe: true}) : of(null), + daoAvatarAddress ? dao.state({ polling: true, pollInterval: GRAPH_POLL_INTERVAL }) : of(null), of(memberState), ethBalance(accountAddress) .pipe(ethErrorHandler()), diff --git a/src/components/Dao/DaoContainer.tsx b/src/components/Dao/DaoContainer.tsx index c1a401421..83f61afbc 100644 --- a/src/components/Dao/DaoContainer.tsx +++ b/src/components/Dao/DaoContainer.tsx @@ -24,6 +24,7 @@ import DaoMembersPage from "./DaoMembersPage"; import * as css from "./Dao.scss"; import DaoLandingPage from "components/Dao/DaoLandingPage"; import i18next from "i18next"; +import { GRAPH_POLL_INTERVAL } from "../../settings"; type IExternalProps = RouteComponentProps; @@ -151,7 +152,7 @@ const SubscribedDaoContainer = withSubscription({ const daoAddress = props.match.params.daoAvatarAddress; const dao = arc.dao(daoAddress); const observable = combineLatest( - dao.state({ subscribe: true, fetchAllData: true }), // DAO state + dao.state({ polling: true, pollInterval: GRAPH_POLL_INTERVAL, fetchAllData: true }), // DAO state dao.members() ); return observable; diff --git a/src/components/Dao/DaoHistoryPage.tsx b/src/components/Dao/DaoHistoryPage.tsx index 467acaf5c..70101e558 100644 --- a/src/components/Dao/DaoHistoryPage.tsx +++ b/src/components/Dao/DaoHistoryPage.tsx @@ -213,7 +213,7 @@ export default withSubscription({ ${Stake.fragments.StakeFields} ${Plugin.baseFragment} `; - await arc.getObservable(prefetchQuery, { subscribe: true }).pipe(first()).toPromise(); + await arc.getObservable(prefetchQuery, { polling: true }).pipe(first()).toPromise(); return combineLatest( dao.proposals({ where: { diff --git a/src/components/Dao/DaoPluginsPage.tsx b/src/components/Dao/DaoPluginsPage.tsx index 6e5c91a3a..b61b36a95 100644 --- a/src/components/Dao/DaoPluginsPage.tsx +++ b/src/components/Dao/DaoPluginsPage.tsx @@ -19,6 +19,7 @@ import * as css from "./DaoPluginsPage.scss"; import ProposalPluginCard from "./ProposalPluginCard"; import SimplePluginCard from "./SimplePluginCard"; import i18next from "i18next"; +import { GRAPH_POLL_INTERVAL } from "../../settings"; const Fade = ({ children, ...props }: any) => ( ): Observable => plugin[0] ? plugin[0].state() : of(null))) ); diff --git a/src/components/Dao/ProposalPluginCard.tsx b/src/components/Dao/ProposalPluginCard.tsx index 004af8036..653228040 100644 --- a/src/components/Dao/ProposalPluginCard.tsx +++ b/src/components/Dao/ProposalPluginCard.tsx @@ -10,6 +10,7 @@ import * as React from "react"; import { Link } from "react-router-dom"; import TrainingTooltip from "components/Shared/TrainingTooltip"; import * as css from "./PluginCard.scss"; +import { GRAPH_POLL_INTERVAL } from "../../settings"; interface IExternalProps { daoState: IDAOState; @@ -100,7 +101,8 @@ export default withSubscription({ }, }, { fetchAllData: true, - subscribe: true, // subscribe to updates of the proposals. We can replace this once https://github.com/daostack/subgraph/issues/326 is done + polling: true, // subscribe to updates of the proposals. + pollInterval: GRAPH_POLL_INTERVAL, }); // the list of boosted proposals }, }); diff --git a/src/components/Daos/DaosPage.tsx b/src/components/Daos/DaosPage.tsx index 976d7d1fa..515da3f2b 100644 --- a/src/components/Daos/DaosPage.tsx +++ b/src/components/Daos/DaosPage.tsx @@ -24,6 +24,7 @@ import i18next from "i18next"; import classNames from "classnames"; import axios from "axios"; import { getNetworkName } from "lib/util"; +import { GRAPH_POLL_INTERVAL } from "../../settings"; type SubscriptionData = [DAO[], DAO[], DAO[]]; @@ -328,13 +329,13 @@ const createSubscriptionObservable = (props: IStateProps, data: SubscriptionData memberDAOsquery, (arc: Arc, r: any) => new DAO(arc, createDaoStateFromQuery(r.dao)), undefined, - { subscribe: true } + { polling: true, pollInterval: GRAPH_POLL_INTERVAL } ) : of([]); // eslint-disable-next-line @typescript-eslint/naming-convention - const followDAOs = followingDAOs.length ? arc.daos({ where: { id_in: followingDAOs }, orderBy: "name", orderDirection: "asc" }, { fetchAllData: true, subscribe: true }) : of([]); + const followDAOs = followingDAOs.length ? arc.daos({ where: { id_in: followingDAOs }, orderBy: "name", orderDirection: "asc" }, { fetchAllData: true, polling: true, pollInterval: GRAPH_POLL_INTERVAL }) : of([]); return combineLatest( - arc.daos({ orderBy: "name", orderDirection: "asc", first: PAGE_SIZE, skip: data ? data[0].length : 0 }, { fetchAllData: true, subscribe: true }), + arc.daos({ orderBy: "name", orderDirection: "asc", first: PAGE_SIZE, skip: data ? data[0].length : 0 }, { fetchAllData: true, polling: true, pollInterval: GRAPH_POLL_INTERVAL }), followDAOs, memberOfDAOs ); diff --git a/src/components/Plugin/ContributionRewardExtRewarders/Competition/Details.tsx b/src/components/Plugin/ContributionRewardExtRewarders/Competition/Details.tsx index 97233c08e..66a320481 100644 --- a/src/components/Plugin/ContributionRewardExtRewarders/Competition/Details.tsx +++ b/src/components/Plugin/ContributionRewardExtRewarders/Competition/Details.tsx @@ -449,7 +449,7 @@ export default withSubscription({ // // sending the query before subscribing seems to resolve a weird cache error - this would ideally be handled in the arc.js // await arc.sendQuery(cacheQuery); // // eslint-disable-next-line @typescript-eslint/no-empty-function - // await arc.getObservable(cacheQuery, {subscribe: true}).subscribe(() => {}); + // await arc.getObservable(cacheQuery, {polling: true}).subscribe(() => {}); // end cache priming return combineLatest( diff --git a/src/components/Plugin/ContributionRewardExtRewarders/Competition/List.tsx b/src/components/Plugin/ContributionRewardExtRewarders/Competition/List.tsx index 575fc6b26..59da4c105 100644 --- a/src/components/Plugin/ContributionRewardExtRewarders/Competition/List.tsx +++ b/src/components/Plugin/ContributionRewardExtRewarders/Competition/List.tsx @@ -9,6 +9,7 @@ import { getArc } from "arc"; import { CompetitionStatusEnum, CompetitionStatus } from "./utils"; import Card from "./Card"; import * as css from "./Competitions.scss"; +import { GRAPH_POLL_INTERVAL } from "../../../../settings"; interface IExternalProps { daoState: IDAOState; @@ -137,7 +138,7 @@ export default withSubscription({ `; const arc = await getArc(); - await arc.sendQuery(cacheQuery, {subscribe: true}); + await arc.sendQuery(cacheQuery, { polling: true, pollInterval: GRAPH_POLL_INTERVAL }); // end cache priming // TODO: next lines can use some cleanup up diff --git a/src/components/Plugin/ContributionRewardExtRewarders/Competition/utils.ts b/src/components/Plugin/ContributionRewardExtRewarders/Competition/utils.ts index 4beed9756..4db7c1008 100644 --- a/src/components/Plugin/ContributionRewardExtRewarders/Competition/utils.ts +++ b/src/components/Plugin/ContributionRewardExtRewarders/Competition/utils.ts @@ -198,10 +198,3 @@ export const getSubmissionVoterHasVoted = (submissionId: string, voterAddress: s return getSubmissionVotes(submissionId, voterAddress, subscribe) .pipe(map((votes: Array) => !!votes.length)); }; - -// export const primeCacheForSubmissionsAndVotes = (): Observable => { -// return combineLatest( -// CompetitionSuggestion.search(getArc(), {}, { subscribe: true, fetchAllData: true }), -// CompetitionVote.search(getArc(), {}, { subscribe: true, fetchAllData: true }) -// ); -// }; diff --git a/src/components/Plugin/ContributionRewardExtRewarders/DetailsPageRouter.tsx b/src/components/Plugin/ContributionRewardExtRewarders/DetailsPageRouter.tsx index 97980ecdf..74f891d96 100644 --- a/src/components/Plugin/ContributionRewardExtRewarders/DetailsPageRouter.tsx +++ b/src/components/Plugin/ContributionRewardExtRewarders/DetailsPageRouter.tsx @@ -5,6 +5,7 @@ import { getArc } from "arc"; import { IDAOState, IContributionRewardExtState, IContributionRewardExtProposalState, ContributionRewardExtProposal, Address } from "@daostack/arc.js"; import Loading from "components/Shared/Loading"; import { getCrxRewarderComponent, CrxRewarderComponentType, getCrxRewarderProposalClass } from "components/Plugin/ContributionRewardExtRewarders/rewardersProps"; +import { GRAPH_POLL_INTERVAL } from "../../../settings"; interface IExternalProps extends RouteComponentProps { currentAccountAddress: Address; @@ -75,6 +76,6 @@ export default withSubscription({ const crxProposalState = await crxProposal.fetchState(); const proposalClass = getCrxRewarderProposalClass(await crxProposalState.plugin.entity.fetchState() as IContributionRewardExtState); const proposal = new proposalClass(arc, props.proposalId); - return proposal.state( { subscribe: true }); + return proposal.state( { polling: true, pollInterval: GRAPH_POLL_INTERVAL }); }, }); diff --git a/src/components/Plugin/PluginContainer.tsx b/src/components/Plugin/PluginContainer.tsx index 166a161d0..ef7a80c7d 100644 --- a/src/components/Plugin/PluginContainer.tsx +++ b/src/components/Plugin/PluginContainer.tsx @@ -25,6 +25,7 @@ import * as css from "./Plugin.scss"; import i18next from "i18next"; import moment = require("moment"); import { formatFriendlyDateForLocalTimezone } from "lib/util"; +import { GRAPH_POLL_INTERVAL } from "../../settings"; interface IDispatchProps { showNotification: typeof showNotification; @@ -286,7 +287,7 @@ const SubscribedPluginContainer = withSubscription({ // eslint-disable-next-line @typescript-eslint/naming-convention { where: { stage_in: [IProposalStage.Boosted, IProposalStage.QuietEndingPeriod, IProposalStage.Queued, IProposalStage.PreBoosted, IProposalStage.Executed] } }, // eslint-disable-next-line @typescript-eslint/no-empty-function - { fetchAllData: true, subscribe: true }).subscribe(() => { }); + { fetchAllData: true, polling: true, pollInterval: GRAPH_POLL_INTERVAL }).subscribe(() => { }); // end cache priming const pluginState = await plugin.fetchState(); @@ -307,7 +308,7 @@ const SubscribedPluginContainer = withSubscription({ orderBy: "closingAt", orderDirection: "desc", }, - { subscribe: true, fetchAllData: true }) + { polling: true, pollInterval: GRAPH_POLL_INTERVAL, fetchAllData: true }) .pipe( // work on each array individually so that toArray can perceive closure on the stream of items in the array mergeMap(proposals => of(proposals).pipe( @@ -322,7 +323,7 @@ const SubscribedPluginContainer = withSubscription({ } return combineLatest( - plugin.fetchState({ subscribe: true }), + plugin.state({ polling: true, pollInterval: GRAPH_POLL_INTERVAL }), // Find the SchemeRegistrar plugin if this dao has one Plugin.search(arc, { where: { dao: props.daoState.id, name: "SchemeFactory" } }).pipe(mergeMap((plugin: Array): Observable => plugin[0] ? plugin[0].state() : of(null))), approvedProposals diff --git a/src/components/Plugin/PluginProposalsPage.tsx b/src/components/Plugin/PluginProposalsPage.tsx index 760f17d32..33bd26919 100644 --- a/src/components/Plugin/PluginProposalsPage.tsx +++ b/src/components/Plugin/PluginProposalsPage.tsx @@ -19,6 +19,7 @@ import TrainingTooltip from "components/Shared/TrainingTooltip"; import ProposalCard from "../Proposal/ProposalCard"; import * as css from "./PluginProposals.scss"; import i18next from "i18next"; +import { GRAPH_POLL_INTERVAL } from "../../settings"; // For infinite scrolling const PAGE_SIZE_QUEUED = 100; @@ -84,7 +85,7 @@ class PluginProposalsPreboosted extends React.Component {proposalsPreBoosted.map((proposal: AnyProposal): any => ( - 0} /> + 0} /> ))} @@ -96,13 +97,7 @@ class PluginProposalsPreboosted extends React.Component Pending Boosting Proposals ({pluginState.numberOfPreBoostedProposals}) - {proposalsPreBoosted.length === 0 - ? -
- -
- : " " - } + {proposalsPreBoosted.length === 0 &&
}
{ @@ -147,7 +142,7 @@ const SubscribedProposalsPreBoosted = withSubscription { @@ -159,7 +154,7 @@ const SubscribedProposalsPreBoosted = withSubscription { {proposalsQueued.map((proposal: AnyProposal): any => ( - 0} /> + 0} /> ))} @@ -186,13 +181,7 @@ class PluginProposalsQueued extends React.Component { Regular Proposals ({pluginState.numberOfQueuedProposals}) - {proposalsQueued.length === 0 - ? -
- -
- : " " - } + {proposalsQueued.length === 0 &&
}
{ @@ -246,7 +235,7 @@ const SubscribedProposalsQueued = withSubscription { {proposalsBoosted.map((proposal: AnyProposal): any => ( - 0} /> + 0} /> ))} @@ -307,13 +296,7 @@ class PluginProposalsPage extends React.Component { Boosted Proposals ({pluginState.numberOfBoostedProposals}) - {proposalsBoosted.length === 0 - ? -
- -
- : " " - } + {proposalsBoosted.length === 0 &&
}
{boostedProposalsHTML} @@ -473,9 +456,9 @@ const SubscribedPluginProposalsPage = withSubscription // eslint-disable-next-line @typescript-eslint/naming-convention where: { scheme: pluginId, stage_in: [IProposalStage.Boosted, IProposalStage.QuietEndingPeriod] }, orderBy: "boostedAt", - }, { subscribe: true }), + }, { polling: true, pollInterval: GRAPH_POLL_INTERVAL }), // big subscription query to make all other subscription queries obsolete - arc.getObservable(bigProposalQuery, { subscribe: true }) as Observable, + arc.getObservable(bigProposalQuery, { polling: true, pollInterval: GRAPH_POLL_INTERVAL }) as Observable, ); }, }); diff --git a/src/components/Proposal/ActionButton.tsx b/src/components/Proposal/ActionButton.tsx index 9dfcf4dd1..fac407e3f 100644 --- a/src/components/Proposal/ActionButton.tsx +++ b/src/components/Proposal/ActionButton.tsx @@ -1,4 +1,4 @@ -import { Address, IDAOState, IContributionRewardProposalState, IProposalOutcome, IProposalStage, IRewardState, Token, AnyProposal } from "@daostack/arc.js"; +import { Address, IDAOState, IContributionRewardProposalState, IProposalOutcome, IProposalStage, IRewardState, Token, IProposalState } from "@daostack/arc.js"; import { executeProposal, redeemProposal } from "actions/arcActions"; import { enableWalletProvider, getArc } from "arc"; import classNames from "classnames"; @@ -26,7 +26,7 @@ interface IExternalProps { detailView?: boolean; expanded?: boolean; parentPage: Page; - proposal: AnyProposal; + proposalState: IProposalState; /** * unredeemed GP rewards owed to the current account */ @@ -48,7 +48,7 @@ interface IDispatchProps { type IProps = IExternalProps & IStateProps & IDispatchProps & ISubscriptionProps<[BN, BN]>; const mapStateToProps = (state: IRootState, ownProps: IExternalProps): IExternalProps & IStateProps => { - const proposalState = ownProps.proposal.coreState; + const proposalState = ownProps.proposalState; return {...ownProps, beneficiaryProfile: proposalState.name === "ContributionReward" ? state.profiles[(proposalState as IContributionRewardProposalState).beneficiary] : null, @@ -76,7 +76,7 @@ class ActionButton extends React.Component { } public async componentDidMount() { - await this.props.proposal.coreState.plugin.entity.fetchState(); + await this.props.proposalState.plugin?.entity.fetchState(); } private handleClickExecute = (type: string) => async (e: any): Promise => { @@ -84,18 +84,18 @@ class ActionButton extends React.Component { if (!await enableWalletProvider({ showNotification: this.props.showNotification })) { return; } - const { currentAccountAddress, daoState, parentPage, proposal } = this.props; + const { currentAccountAddress, daoState, parentPage, proposalState } = this.props; - await this.props.executeProposal(daoState.address, proposal.id, currentAccountAddress); + await this.props.executeProposal(daoState.address, proposalState.id, currentAccountAddress); Analytics.track("Transition Proposal", { "DAO Address": daoState.address, "DAO Name": daoState.name, "Origin": parentPage, - "Proposal Hash": proposal.id, - "Proposal Title": proposal.coreState.title, - "Plugin Address": proposal.coreState.plugin.entity.coreState.address, - "Plugin Name": proposal.coreState.plugin.entity.coreState.name, + "Proposal Hash": proposalState.id, + "Proposal Title": proposalState.title, + "Plugin Address": proposalState.plugin?.entity.coreState.address, + "Plugin Name": proposalState.plugin?.entity.coreState.name, "Type": type, }); @@ -126,7 +126,7 @@ class ActionButton extends React.Component { expired, expanded, parentPage, - proposal, + proposalState, /** * unredeemed GP rewards owed to the current account */ @@ -159,8 +159,8 @@ class ActionButton extends React.Component { let daoLacksRequiredCrRewards = false; let daoLacksAllRequiredCrRewards = false; let contributionRewards; - if (proposal.coreState.name === "ContributionReward") { - const crState = proposal.coreState as IContributionRewardProposalState; + if (proposalState.name === "ContributionReward") { + const crState = proposalState as IContributionRewardProposalState; /** * unredeemed by the beneficiary */ @@ -200,9 +200,11 @@ class ActionButton extends React.Component { * * We'll display the redeem button even if the CR beneficiary is not the current account. */ - const displayRedeemButton = proposal.coreState.executedAt && + + const displayRedeemButton = ((proposalState as any).coreState?.executedAt || proposalState.executedAt) && ((currentAccountNumUnredeemedGpRewards > 0) || - ((proposal.coreState.winningOutcome === IProposalOutcome.Pass) && (beneficiaryNumUnredeemedCrRewards > 0))); + ((((proposalState as any).coreState?.winningOutcome === IProposalOutcome.Pass) || proposalState.winningOutcome) && + (beneficiaryNumUnredeemedCrRewards > 0))); const redemptionsTip = RedemptionsTip({ canRewardNone, @@ -212,7 +214,7 @@ class ActionButton extends React.Component { daoState: daoState, gpRewards, id: rewards ? rewards.id : "0", - proposal: proposal, + proposal: proposalState.proposal?.entity as any, }); const redeemButtonClass = classNames({ @@ -236,28 +238,28 @@ class ActionButton extends React.Component { daoState={daoState} parentPage={parentPage} effectText={redemptionsTip} - proposalState={proposal.coreState} + proposalState={proposalState} multiLineMsg /> : "" } - { proposal.coreState.stage === IProposalStage.Queued && proposal.coreState.upstakeNeededToPreBoost.ltn(0) ? + { proposalState.stage === IProposalStage.Queued && proposalState.upstakeNeededToPreBoost.ltn(0) ? : - proposal.coreState.stage === IProposalStage.PreBoosted && expired && proposal.coreState.downStakeNeededToQueue.lten(0) ? + proposalState.stage === IProposalStage.PreBoosted && expired && proposalState.downStakeNeededToQueue.lten(0) ? : - proposal.coreState.stage === IProposalStage.PreBoosted && expired ? + proposalState.stage === IProposalStage.PreBoosted && expired ? : - (proposal.coreState.stage === IProposalStage.Boosted || proposal.coreState.stage === IProposalStage.QuietEndingPeriod) && expired ? + (proposalState.stage === IProposalStage.Boosted || proposalState.stage === IProposalStage.QuietEndingPeriod) && expired ? + + + + + +
+ + ); + }} + /> + + ); + } +} + +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..95a04b256 100644 --- a/src/components/Proposal/Create/PluginForms/PluginInitializeFields.tsx +++ b/src/components/Proposal/Create/PluginForms/PluginInitializeFields.tsx @@ -9,6 +9,7 @@ import { IFormValues } from "./CreatePluginManagerProposal"; import * as css from "../CreateProposal.scss"; import { Form, ErrorMessage, Field } from "formik"; import * as validators from "./Validators"; +import i18next from "i18next"; interface IProps { pluginName: keyof typeof PLUGIN_NAMES | ""; @@ -125,32 +126,38 @@ const ContributionRewardExtFields = () => ( const FundingRequest = () => (
- {fieldView("FundingRequest", "Funding Token", "fundingToken")} + {fieldView("FundingRequest", "Funding Token", "fundingToken", (address: string) => validators.address(address, true))} {GenesisProtocolFields("FundingRequest.votingParams")}
); const Join = () => (
- {fieldView("Join", "Funding Token", "fundingToken")} - {fieldView("Join", "Minimum Join Fee", "minFeeToJoin")} - {fieldView("Join", "Initial Reputation", "memberReputation")} - {fieldView("Join", "Funding Goal", "fundingGoal")} - {fieldView("Join", "Deadline", "fundingGoalDeadline")} - {fieldView("Join", "Allow Rage Quit", "rageQuitEnable")} + {fieldView("Join", "Funding Token", "fundingToken", (address: string) => validators.address(address, true))} + {fieldView("Join", "Minimum Join Fee", "minFeeToJoin", validators.validNumber)} + {fieldView("Join", "Initial Reputation", "memberReputation", validators.validNumber)} + {fieldView("Join", "Funding Goal", "fundingGoal", validators.validNumber)} + {fieldView("Join", "Deadline", "fundingGoalDeadline", validators.validNumber)} + {GenesisProtocolFields("Join.votingParams")} +
+); + +const TokenTrade = () => ( +
+ {GenesisProtocolFields("TokenTrade.votingParams")}
); const SchemeRegistrarFields = () => (
- - Add Plugin Vote Params - - {GenesisProtocolFields("SchemeRegistrar.votingParamsRegister")} - - Remove Plugin Vote Params - - {GenesisProtocolFields("SchemeRegistrar.votingParamsRemove")} +
+ {i18next.t("Add Params")} + {GenesisProtocolFields("SchemeRegistrar.votingParamsRegister")} +
+
+ {i18next.t("Remove Params")} + {GenesisProtocolFields("SchemeRegistrar.votingParamsRemove")} +
); @@ -174,6 +181,7 @@ const fieldsMap = { ContributionRewardExt: ContributionRewardExtFields, FundingRequest: FundingRequest, Join: Join, + TokenTrade: TokenTrade, SchemeRegistrar: SchemeRegistrarFields, SchemeFactory: PluginManagerFields, ReputationFromToken: ReputationFromTokenFields, diff --git a/src/components/Proposal/ProposalCard.tsx b/src/components/Proposal/ProposalCard.tsx index 912168398..19eb2d065 100644 --- a/src/components/Proposal/ProposalCard.tsx +++ b/src/components/Proposal/ProposalCard.tsx @@ -1,4 +1,4 @@ -import { Address, IDAOState, IProposalStage, Vote, AnyProposal } from "@daostack/arc.js"; +import { Address, IDAOState, IProposalStage, Vote, IProposalState } from "@daostack/arc.js"; import classNames from "classnames"; import AccountPopup from "components/Account/AccountPopup"; import AccountProfileName from "components/Account/AccountProfileName"; @@ -27,7 +27,7 @@ import * as css from "./ProposalCard.scss"; interface IExternalProps { currentAccountAddress: Address; daoState: IDAOState; - proposal: AnyProposal; + proposal: IProposalState; suppressTrainingTooltips?: boolean; } @@ -59,13 +59,12 @@ export default class ProposalCard extends React.Component { daoEthBalance, expired, member, - proposal, + proposalState, rewards, stakes, votes, } = props; - const proposalState = proposal.coreState; const tags = proposalState.tags; let currentAccountVote = 0; @@ -166,7 +165,7 @@ export default class ProposalCard extends React.Component { currentAccountAddress={currentAccountAddress} daoState={daoState} daoEthBalance={daoEthBalance} - proposal={proposal} + proposalState={proposal} rewards={rewards} expired={expired} parentPage={Page.PluginProposals} diff --git a/src/components/Proposal/ProposalData.tsx b/src/components/Proposal/ProposalData.tsx index 15459e02e..c7dab9cb7 100644 --- a/src/components/Proposal/ProposalData.tsx +++ b/src/components/Proposal/ProposalData.tsx @@ -1,4 +1,4 @@ -import { Address, AnyProposal, IProposalState, IDAOState, IMemberState, IRewardState, Reward, Stake, Vote, Proposal, Member, IContributionRewardProposalState } from "@daostack/arc.js"; +import { Address, IProposalState, IDAOState, IMemberState, IRewardState, Reward, Stake, Vote, Proposal, Member, IContributionRewardProposalState } from "@daostack/arc.js"; import { getArc } from "arc"; import { ethErrorHandler } from "lib/util"; @@ -12,6 +12,7 @@ import { closingTime } from "lib/proposalHelpers"; import { IProfileState } from "reducers/profilesReducer"; import { combineLatest, of, Observable } from "rxjs"; import { map, mergeMap } from "rxjs/operators"; +import { GRAPH_POLL_INTERVAL } from "../../settings"; import * as css from "./ProposalCard.scss"; @@ -31,7 +32,7 @@ interface IStateProps { creatorProfile?: IProfileState; } -type SubscriptionData = [AnyProposal, IProposalState, Vote[], Stake[], IRewardState, IMemberState|null, BN, BN, IDAOState]; +type SubscriptionData = [IProposalState, Vote[], Stake[], IRewardState, IMemberState|null, BN, BN, IDAOState]; type IPreProps = IStateProps & IExternalProps & ISubscriptionProps; type IProps = IStateProps & IExternalProps & ISubscriptionProps; @@ -43,16 +44,16 @@ export interface IInjectedProposalProps { daoEthBalance: BN; expired: boolean; member: IMemberState; - proposal: AnyProposal; + proposalState: IProposalState; rewards: IRewardState; stakes: Stake[]; votes: Vote[]; } const mapStateToProps = (state: IRootState, ownProps: IExternalProps & ISubscriptionProps): IPreProps => { - const proposal = ownProps.data[0]; - const proposalState = proposal ? proposal.coreState : null; - const crState = proposal ? proposal.coreState as IContributionRewardProposalState : null; + const proposalState = ownProps.data[0]; + //const proposalState = proposal ? proposal.coreState : null; + const crState = proposalState as IContributionRewardProposalState; return { ...ownProps, @@ -72,10 +73,10 @@ class ProposalData extends React.Component { super(props); if (props.data && props.data[0]) { - const proposal = props.data[0]; + const proposalState = props.data[0]; this.state = { - expired: proposal.coreState ? closingTime(proposal.coreState).isSameOrBefore(moment()) : false, + expired: proposalState ? closingTime(proposalState).isSameOrBefore(moment()) : false, }; } else { this.state = { @@ -91,7 +92,7 @@ class ProposalData extends React.Component { // Expire proposal in real time // Don't schedule timeout if its too long to wait, because browser will fail and trigger the timeout immediately - const millisecondsUntilExpires = closingTime(this.props.data[0].coreState).diff(moment()); + const millisecondsUntilExpires = closingTime(this.props.data[0]).diff(moment()); if (!this.state.expired && millisecondsUntilExpires < 2147483647) { this.expireTimeout = setTimeout(() => { this.setState({ expired: true });}, millisecondsUntilExpires); } @@ -106,7 +107,7 @@ class ProposalData extends React.Component { return <>; } - const [proposal,, votes, stakes, rewards, member, currentAccountGenBalance, currentAccountGenAllowance, daoState] = this.props.data; + const [proposal, votes, stakes, rewards, member, currentAccountGenBalance, currentAccountGenAllowance, daoState] = this.props.data; const { beneficiaryProfile, creatorProfile } = this.props; const daoEthBalance = new BN(daoState.ethBalance); @@ -118,7 +119,7 @@ class ProposalData extends React.Component { daoEthBalance, expired: this.state.expired, member, - proposal, + proposalState: proposal, rewards, stakes, votes, @@ -154,11 +155,10 @@ export default withSubscription({ return combineLatest( - of(proposal), - proposal.state({ subscribe: props.subscribeToProposalDetails }), // state of the current proposal - proposal.votes({where: { voter: currentAccountAddress }}, { subscribe: props.subscribeToProposalDetails }), - proposal.stakes({where: { staker: currentAccountAddress }}, { subscribe: props.subscribeToProposalDetails }), - proposal.rewards({ where: {beneficiary: currentAccountAddress}}, { subscribe: props.subscribeToProposalDetails }) + proposal.state({ polling: true, pollInterval: GRAPH_POLL_INTERVAL }), // state of the current proposal + proposal.votes({where: { voter: currentAccountAddress }}, { polling: true, pollInterval: GRAPH_POLL_INTERVAL }), + proposal.stakes({where: { staker: currentAccountAddress }}, { polling: true, pollInterval: GRAPH_POLL_INTERVAL }), + proposal.rewards({ where: {beneficiary: currentAccountAddress}}, { polling: true, pollInterval: GRAPH_POLL_INTERVAL }) .pipe(map((rewards: Reward[]): Reward => rewards.length === 1 && rewards[0] || null)) .pipe(mergeMap(((reward: Reward): Observable => reward ? reward.state() : of(null)))), @@ -170,19 +170,18 @@ export default withSubscription({ .pipe(ethErrorHandler()), arc.allowance(currentAccountAddress, spender) .pipe(ethErrorHandler()), - dao.state({ subscribe: true }), + dao.state({ polling: true, pollInterval: GRAPH_POLL_INTERVAL }), ); } else { return combineLatest( - of(proposal), - proposal.state({ subscribe: props.subscribeToProposalDetails }), // state of the current proposal + proposal.state({ polling: true, pollInterval: GRAPH_POLL_INTERVAL }), // state of the current proposal of([]), // votes of([]), // stakes of(null), // rewards of(null), // current account member state of(new BN(0)), // current account gen balance of(null), // current account GEN allowance - dao.state({ subscribe: true }), + dao.state({ polling: true, pollInterval: GRAPH_POLL_INTERVAL }), ); } }, diff --git a/src/components/Proposal/ProposalDetailsPage.tsx b/src/components/Proposal/ProposalDetailsPage.tsx index 254b1f009..11fad0079 100644 --- a/src/components/Proposal/ProposalDetailsPage.tsx +++ b/src/components/Proposal/ProposalDetailsPage.tsx @@ -76,10 +76,10 @@ class ProposalDetailsPage extends React.Component { "Page Name": Page.ProposalDetails, "DAO Address": this.props.daoState.address, "DAO Name": this.props.daoState.name, - "Proposal Hash": this.props.proposal.id, - "Proposal Title": this.props.proposal.coreState.title, - "Plugin Address": this.props.proposal.coreState.plugin.id, - "Plugin Name": this.props.proposal.coreState.plugin.entity.coreState.name, + "Proposal Hash": this.props.proposalState.id, + "Proposal Title": this.props.proposalState.title, + "Plugin Address": this.props.proposalState.plugin.id, + "Plugin Name": this.props.proposalState.plugin.entity.coreState.name, }); if (this.props.votes.length > 0) { @@ -87,7 +87,7 @@ class ProposalDetailsPage extends React.Component { newState.currentAccountVote = currentVote.coreState.outcome; } - newState.crxContractName = rewarderContractName(this.props.proposal.coreState.plugin.entity.coreState as IContributionRewardExtState); + newState.crxContractName = rewarderContractName(this.props.proposalState.plugin.entity.coreState as IContributionRewardExtState); this.setState(newState); } @@ -120,21 +120,19 @@ class ProposalDetailsPage extends React.Component { daoState, expired, member, - proposal, + proposalState, rewards, stakes, votes, } = this.props; - const proposalState = proposal.coreState; - if (daoState.id !== proposalState.dao.id) { return
`The given proposal does not belong to ${daoState.name}. Please check the browser url.`
; } const tags = proposalState.tags; - const url = ensureHttps(proposal.coreState.url); + const url = ensureHttps(proposalState.url); this.disqusConfig.title = proposalState.title; this.disqusConfig.url = process.env.BASE_URL + this.props.location.pathname; @@ -150,8 +148,8 @@ class ProposalDetailsPage extends React.Component { return (
{pluginName(proposalState.plugin.entity.coreState, proposalState.plugin.entity.coreState.address)} - {humanProposalTitle(proposalState, 40)} -
+ {humanProposalTitle(proposalState, 40)} +
@@ -165,7 +163,7 @@ class ProposalDetailsPage extends React.Component { daoEthBalance={daoEthBalance} detailView parentPage={Page.ProposalDetails} - proposal={proposal} + proposalState={proposalState} rewards={rewards} expired={expired} /> @@ -173,13 +171,13 @@ class ProposalDetailsPage extends React.Component { { (this.state.crxContractName) ?
{ - Go to {this.state.crxContractName} > + Go to {this.state.crxContractName} > }
: "" }

- {humanProposalTitle(proposalState)} + {humanProposalTitle(proposalState)}

@@ -248,7 +246,7 @@ class ProposalDetailsPage extends React.Component { Share -
+
@@ -337,7 +335,7 @@ class ProposalDetailsPage extends React.Component { {this.state.showShareModal ? : "" }
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 { } else if (proposal.coreState.name.includes("SchemeRegistrar")) { const state = proposal.coreState as IPluginRegistrarProposalState; return ; + } else if (proposal.coreState.name.includes("TokenTrade")) { + const state = proposal.coreState as ITokenTradeProposalState; + return ; } else if (proposal.coreState.name.includes("SchemeFactory")) { const state = proposal.coreState as IPluginManagerProposalState; return ; diff --git a/src/components/Proposal/ProposalSummary/ProposalSummaryPluginManager.tsx b/src/components/Proposal/ProposalSummary/ProposalSummaryPluginManager.tsx index 918b93b57..77d9d50d5 100644 --- a/src/components/Proposal/ProposalSummary/ProposalSummaryPluginManager.tsx +++ b/src/components/Proposal/ProposalSummary/ProposalSummaryPluginManager.tsx @@ -73,6 +73,9 @@ class ProposalSummary extends React.Component { else if (pluginName === "ContributionRewardExt"){ pluginName = decodedData.params[7].value; // Rewarder name } + else if (pluginName === "SchemeFactory"){ + pluginName = "Plugin Manager"; + } } const proposalSummaryClass = classNames({ 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 { + + 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 ( +
+ + { sendToken && +
+
+ {i18next.t("Send to DAO")}: +
+ + + + + + + {receiveToken} +
+ } + { receiveToken && +
+
+ {i18next.t("Receive from DAO")}: +
+ {receiveToken} + + + + + + +
+ } +
+
+ ); + + } +} diff --git a/src/components/Proposal/RedemptionsString.tsx b/src/components/Proposal/RedemptionsString.tsx index a2fcc93bc..63da0403e 100644 --- a/src/components/Proposal/RedemptionsString.tsx +++ b/src/components/Proposal/RedemptionsString.tsx @@ -1,4 +1,4 @@ -import { Address, IDAOState, AnyProposal, IRewardState, IContributionRewardProposalState } from "@daostack/arc.js"; +import { Address, IDAOState, IProposalState, IRewardState, IContributionRewardProposalState } from "@daostack/arc.js"; import BN = require("bn.js"); import Reputation from "components/Account/Reputation"; @@ -9,7 +9,7 @@ import * as css from "./RedemptionsString.scss"; interface IProps { currentAccountAddress: Address; daoState: IDAOState; - proposal: AnyProposal; + proposal: IProposalState; rewards: IRewardState | null; separator?: string; } @@ -42,8 +42,8 @@ export default class RedemptionsString extends React.Component { } } - const proposalState = proposal.coreState; - const contributionReward = proposal.coreState as IContributionRewardProposalState; + const proposalState = proposal; + const contributionReward = proposal as IContributionRewardProposalState; if ((proposalState.name === "ContributionReward") && contributionReward && currentAccountAddress === contributionReward.beneficiary) { const rewards = getCRRewards(contributionReward); diff --git a/src/components/Proposal/RedemptionsTip.tsx b/src/components/Proposal/RedemptionsTip.tsx index 108037338..9bbbd4631 100644 --- a/src/components/Proposal/RedemptionsTip.tsx +++ b/src/components/Proposal/RedemptionsTip.tsx @@ -1,4 +1,4 @@ -import { Address, IDAOState, IProposalOutcome, AnyProposal, IContributionRewardProposalState } from "@daostack/arc.js"; +import { Address, IDAOState, IProposalOutcome, IProposalState, IContributionRewardProposalState } from "@daostack/arc.js"; import Reputation from "components/Account/Reputation"; import { baseTokenName, formatTokens, fromWei, genName, tokenDecimals, tokenSymbol, AccountClaimableRewardsType } from "lib/util"; import * as React from "react"; @@ -13,12 +13,12 @@ interface IProps { // non-zero GP rewards of current user, payable or not gpRewards: AccountClaimableRewardsType; id: string; - proposal: AnyProposal; + proposal: IProposalState; } export default (props: IProps): JSX.Element => { const { canRewardNone, canRewardOnlySome, currentAccountAddress, contributionRewards, daoState, gpRewards, id, proposal } = props; - const proposalState = proposal.coreState; + const proposalState = proposal; const messageDiv = (canRewardNone || canRewardOnlySome) ?
@@ -70,7 +70,7 @@ export default (props: IProps): JSX.Element => { if (contributionRewards) { if (proposalState.winningOutcome === IProposalOutcome.Pass && proposalState.name === "ContributionReward") { if (Object.keys(contributionRewards).length > 0) { - const contributionReward = proposal.coreState as IContributionRewardProposalState; + const contributionReward = proposal as IContributionRewardProposalState; ContributionRewardDiv =
{(currentAccountAddress && currentAccountAddress === contributionReward.beneficiary.toLowerCase()) ? diff --git a/src/components/Proposal/Staking/StakeButtons.scss b/src/components/Proposal/Staking/StakeButtons.scss index 99a9747af..76cb93e90 100644 --- a/src/components/Proposal/Staking/StakeButtons.scss +++ b/src/components/Proposal/Staking/StakeButtons.scss @@ -209,9 +209,10 @@ .enablePredictions { button { - width: 120px; - white-space: nowrap; + width: fit-content; height: auto; + padding-left: 10px; + padding-right: 10px; } } } diff --git a/src/components/Redemptions/RedemptionsButton.tsx b/src/components/Redemptions/RedemptionsButton.tsx index e562a1cf9..503dc6e65 100644 --- a/src/components/Redemptions/RedemptionsButton.tsx +++ b/src/components/Redemptions/RedemptionsButton.tsx @@ -1,4 +1,4 @@ -import { Address, AnyProposal, Proposal } from "@daostack/arc.js"; +import { Address, IProposalState, Proposal } from "@daostack/arc.js"; import { getArc } from "arc"; import withSubscription, { ISubscriptionProps } from "components/Shared/withSubscription"; import * as React from "react"; @@ -12,7 +12,7 @@ interface IExternalProps { currentAccountAddress?: Address; } -type IProps = IExternalProps & ISubscriptionProps; +type IProps = IExternalProps & ISubscriptionProps; class RedemptionsButton extends React.Component { private menu = React.createRef() diff --git a/src/components/Redemptions/RedemptionsMenu.tsx b/src/components/Redemptions/RedemptionsMenu.tsx index 496d69122..56e1f468b 100644 --- a/src/components/Redemptions/RedemptionsMenu.tsx +++ b/src/components/Redemptions/RedemptionsMenu.tsx @@ -1,4 +1,4 @@ -import { Address, AnyProposal, IDAOState, IRewardState, Reward, IContributionRewardProposalState } from "@daostack/arc.js"; +import { Address, IDAOState, IRewardState, Reward, IContributionRewardProposalState, IProposalState } from "@daostack/arc.js"; import { enableWalletProvider, getArc } from "arc"; import { redeemProposal } from "actions/arcActions"; @@ -18,9 +18,10 @@ import { IProfileState } from "reducers/profilesReducer"; import { combineLatest, Observable, of } from "rxjs"; import { defaultIfEmpty, map, mergeMap } from "rxjs/operators"; import * as css from "./RedemptionsMenu.scss"; +import { GRAPH_POLL_INTERVAL } from "../../settings"; interface IExternalProps { - redeemableProposals: AnyProposal[]; + redeemableProposals: IProposalState[]; handleClose: () => void; } @@ -45,7 +46,7 @@ const mapDispatchToProps = { showNotification, }; -type IProps = IExternalProps & IStateProps & IDispatchProps & ISubscriptionProps; +type IProps = IExternalProps & IStateProps & IDispatchProps & ISubscriptionProps; class RedemptionsMenu extends React.Component { public render(): RenderOutput { @@ -111,12 +112,12 @@ const SubscribedRedemptionsMenu = withSubscription({ createObservable: (props: IExternalProps) => { return of( props.redeemableProposals - ).pipe(defaultIfEmpty([])); + ).pipe(defaultIfEmpty([])); }, }); interface IMenuItemProps { - proposal: AnyProposal; + proposal: IProposalState; currentAccountAddress: Address; handleClose: () => void; } @@ -126,8 +127,8 @@ class MenuItem extends React.Component { const { proposal } = this.props; return
- - {humanProposalTitle(proposal.coreState)} + + {humanProposalTitle(proposal)}
@@ -143,8 +144,8 @@ interface IMenuItemContentStateProps { const mapStateToItemContentProps = (state: IRootState, ownProps: IMenuItemProps) => { const { proposal } = ownProps; - const proposalState = proposal.coreState; - const contributionReward = proposal.coreState as IContributionRewardProposalState; + const proposalState = proposal; + const contributionReward = proposal as IContributionRewardProposalState; return { ...ownProps, beneficiaryProfile: proposalState.name === "ContributionReward" ? state.profiles[contributionReward.beneficiary] : null, @@ -159,7 +160,7 @@ class MenuItemContent extends React.Component { const [daoState, rewards] = data; return { daoEthBalance={new BN(daoState.ethBalance)} expanded expired - proposal={proposal} + proposalState={proposal} rewards={rewards} parentPage={Page.RedemptionsMenu} onClick={handleClose} @@ -197,12 +198,12 @@ const SubscribedMenuItemContent = withSubscription({ createObservable: async (props: IMenuItemProps) => { const { currentAccountAddress, proposal } = props; const arc = getArc(); - const dao = arc.dao(proposal.coreState.dao.id); - const rewards = proposal.rewards({ where: { beneficiary: currentAccountAddress }}) + const dao = arc.dao((proposal as any).coreState.dao.id); + const rewards = (proposal as any).rewards({ where: { beneficiary: currentAccountAddress }}) .pipe(map((rewards: Reward[]): Reward => rewards.length === 1 && rewards[0] || null)) .pipe(mergeMap(((reward: Reward): Observable => reward ? reward.state() : of(null)))); // subscribe to dao to get DAO reputation supply updates - return combineLatest(dao.state({ subscribe: true }), rewards); + return combineLatest(dao.state({ polling: true, pollInterval: GRAPH_POLL_INTERVAL }), rewards); }, }); diff --git a/src/components/Redemptions/RedemptionsPage.tsx b/src/components/Redemptions/RedemptionsPage.tsx index 8928be86c..04142be95 100644 --- a/src/components/Redemptions/RedemptionsPage.tsx +++ b/src/components/Redemptions/RedemptionsPage.tsx @@ -1,4 +1,4 @@ -import { Address, IContributionRewardProposalState, IDAOState, IRewardState, Proposal, DAO, AnyProposal } from "@daostack/arc.js"; +import { Address, IContributionRewardProposalState, IDAOState, IRewardState, Proposal, DAO, IProposalState } from "@daostack/arc.js"; import { enableWalletProvider, getArc } from "arc"; import { redeemProposal } from "actions/arcActions"; @@ -20,6 +20,7 @@ import { of } from "rxjs"; import { map, first } from "rxjs/operators"; import ProposalCard from "../Proposal/ProposalCard"; import * as css from "./RedemptionsPage.scss"; +import { GRAPH_POLL_INTERVAL } from "../../settings"; interface IStateProps { currentAccountAddress: string; @@ -48,7 +49,7 @@ interface IProposalData { dao: IDAOData; gpRewards: IRewardState[]; contributionRewardState: IContributionRewardProposalState; - proposal: AnyProposal; + proposal: IProposalState; } class RedemptionsPage extends React.Component { @@ -273,7 +274,7 @@ const SubscribedRedemptionsPage = withSubscription({ } ${DAO.fragments.DAOFields} `; - const proposals = await arc.getObservable(query, { subscribe: true }) + const proposals = await arc.getObservable(query, { polling: true, pollInterval: GRAPH_POLL_INTERVAL }) .pipe(map(async (result: any) => { const proposals: IProposalData[] = result.data.proposals; diff --git a/src/components/Shared/PreTransactionModal.tsx b/src/components/Shared/PreTransactionModal.tsx index 197776ea3..ed0a8cab1 100644 --- a/src/components/Shared/PreTransactionModal.tsx +++ b/src/components/Shared/PreTransactionModal.tsx @@ -284,8 +284,8 @@ class PreTransactionModal extends React.Component { "DAO Name": daoState.name, "Proposal Hash": proposalState.id, "Proposal Title": proposalState.title, - "Plugin Address": proposalState.plugin.entity.coreState.address, - "Plugin Name": proposalState.plugin.entity.coreState.name, + "Plugin Address": (proposalState as any).coreState?.address, + "Plugin Name": (proposalState as any).coreState?.name, }); break; case ActionTypes.Execute: diff --git a/src/layouts/Header.tsx b/src/layouts/Header.tsx index 9a32a8cb0..3d53a181a 100644 --- a/src/layouts/Header.tsx +++ b/src/layouts/Header.tsx @@ -26,6 +26,7 @@ import * as css from "./App.scss"; import ProviderConfigButton from "layouts/ProviderConfigButton"; import Tooltip from "rc-tooltip"; import i18next from "i18next"; +import { GRAPH_POLL_INTERVAL } from "../settings"; interface IExternalProps extends RouteComponentProps { } @@ -303,7 +304,7 @@ const SubscribedHeader = withSubscription({ if (props.daoAvatarAddress) { const arc = getArc(); // subscribe if only to get DAO reputation supply updates - return arc.dao(props.daoAvatarAddress).state({ subscribe: true }); + return arc.dao(props.daoAvatarAddress).state({ polling: true, pollInterval: GRAPH_POLL_INTERVAL }); } else { return of(null); } diff --git a/src/layouts/SidebarMenu.tsx b/src/layouts/SidebarMenu.tsx index 4b70c2e3d..66fbc5d6e 100644 --- a/src/layouts/SidebarMenu.tsx +++ b/src/layouts/SidebarMenu.tsx @@ -21,6 +21,7 @@ import { of } from "rxjs"; import Tooltip from "rc-tooltip"; import * as css from "./SidebarMenu.scss"; import i18next from "i18next"; +import { GRAPH_POLL_INTERVAL } from "../settings"; type IExternalProps = RouteComponentProps; @@ -336,7 +337,7 @@ const SubscribedSidebarMenu = withSubscription({ createObservable: (props: IProps) => { if (props.daoAvatarAddress) { const arc = getArc(); - return arc.dao(props.daoAvatarAddress).state({ subscribe: true } ); + return arc.dao(props.daoAvatarAddress).state({ polling: true, pollInterval: GRAPH_POLL_INTERVAL } ); } else { return of(null); } 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/src/settings.ts b/src/settings.ts index 53a231216..2d63bbcfa 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -2,6 +2,7 @@ export const ETHDENVER_OPTIMIZATION = true; // if this is true, we do get the contractInfos from a locally stored file in ./data instead of from the subgraph export const USE_CONTRACTINFOS_CACHE = false; +export const GRAPH_POLL_INTERVAL = 30000; import BurnerConnectProvider from "@burner-wallet/burner-connect-provider"; import WalletConnectProvider from "@walletconnect/web3-provider"; const Torus = require("@toruslabs/torus-embed"); 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(); + }); + +});