diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..28f6cc0d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,31 @@ +matrix: + include: + # Frontend + - language: node_js + node_js: 8.11.4 + before_install: + - cd frontend/ + install: yarn + script: + - yarn run lint + - yarn run tsc + # Backend + - language: python + python: 3.6 + before_install: + - cd backend/ + - cp .env.example .env + install: pip install -r requirements/dev.txt + script: + - flask test + # Contracts + - language: node_js + node_js: 8.11.4 + before_install: + - cd contract/ + install: yarn && yarn add global truffle ganache-cli + before_script: + - ganache-cli > /dev/null & + - sleep 10 + script: + - yarn run test diff --git a/backend/tests/test_example.py b/backend/tests/test_example.py new file mode 100644 index 00000000..3c318252 --- /dev/null +++ b/backend/tests/test_example.py @@ -0,0 +1,4 @@ +"""Sample test for CI""" + +def test_runs(): + assert True \ No newline at end of file diff --git a/frontend/client/api/api.ts b/frontend/client/api/api.ts index 790cea97..4e5bd0f1 100644 --- a/frontend/client/api/api.ts +++ b/frontend/client/api/api.ts @@ -1,5 +1,6 @@ import axios from './axios'; import { Proposal } from 'modules/proposals/reducers'; +import { PROPOSAL_CATEGORY } from './constants'; export function getProposals(): Promise<{ data: Proposal[] }> { return axios.get('/api/proposals/'); @@ -18,11 +19,12 @@ export function getProposalUpdates(proposalId: number | string) { } export function postProposal(payload: { - accountAddress; - crowdFundContractAddress; - content; - title; - milestones; + accountAddress: string; + crowdFundContractAddress: string; + content: string; + title: string; + category: PROPOSAL_CATEGORY; + milestones: object[]; // TODO: Type me }) { return axios.post(`/api/proposals/create`, payload); } diff --git a/frontend/client/components/CreateProposal/index.tsx b/frontend/client/components/CreateProposal/index.tsx index 45fcfbef..4cee2f33 100644 --- a/frontend/client/components/CreateProposal/index.tsx +++ b/frontend/client/components/CreateProposal/index.tsx @@ -1,6 +1,6 @@ // TODO: Make each section its own page. Reduce size of this component! import React from 'react'; -import Web3Container from 'lib/Web3Container'; +import Web3Container, { Web3RenderProps } from 'lib/Web3Container'; import { bindActionCreators, Dispatch } from 'redux'; import { connect } from 'react-redux'; import { compose } from 'recompose'; @@ -17,6 +17,23 @@ import { getAmountError } from 'utils/validators'; import MarkdownEditor from 'components/MarkdownEditor'; import * as Styled from './styled'; +interface StateProps { + crowdFundLoading: AppState['web3']['crowdFundLoading']; + crowdFundError: AppState['web3']['crowdFundError']; + crowdFundCreatedAddress: AppState['web3']['crowdFundCreatedAddress']; +} + +interface DispatchProps { + createCrowdFund: typeof web3Actions['createCrowdFund']; +} + +interface Web3Props { + web3: Web3RenderProps['web3']; + contract: Web3RenderProps['contracts'][0]; +} + +type Props = StateProps & DispatchProps & Web3Props; + interface Errors { title?: string; amountToRaise?: string; @@ -61,7 +78,7 @@ function milestoneToMilestoneAmount(milestone: Milestone, raiseGoal: number) { return computePercentage(raiseGoal, milestone.payoutPercent); } -class CreateProposal extends React.Component { +class CreateProposal extends React.Component { constructor(props: any) { super(props); this.state = { ...DEFAULT_STATE }; @@ -172,7 +189,6 @@ class CreateProposal extends React.Component { durationInMinutes: deadline, milestoneVotingPeriodInMinutes: milestoneDeadline, immediateFirstMilestonePayout, - category, }; createCrowdFund(contract, contractData, backendData); @@ -516,7 +532,7 @@ const withConnect = connect( mapDispatchToProps, ); -const ConnectedCreateProposal = compose(withConnect)(CreateProposal); +const ConnectedCreateProposal = compose(withConnect)(CreateProposal); export default () => ( { - e.preventDefault(); - this.props.form.validateFields((err, values) => { - if (!err) { - console.log('Received values of form: ', values); - } - }); - }; - - normFile = e => { - console.log('Upload event:', e); - if (Array.isArray(e)) { - return e; - } - return e && e.fileList; - }; - - render() { - const { getFieldDecorator } = this.props.form; - const formItemLayout = { - labelCol: { span: 6 }, - wrapperCol: { span: 14 }, - }; - return ( -
- - {getFieldDecorator('input-number', { initialValue: 3 })( - , - )} - machines - - - - {getFieldDecorator('switch', { valuePropName: 'checked' })()} - - - - {getFieldDecorator('slider')( - , - )} - - - - {getFieldDecorator('radio-group')( - - item 1 - item 2 - item 3 - , - )} - - - - {getFieldDecorator('radio-button')( - - item 1 - item 2 - item 3 - , - )} - - - -
- {getFieldDecorator('dragger', { - valuePropName: 'fileList', - getValueFromEvent: this.normFile, - })( - -

- -

-

- Click or drag file to this area to upload -

-

Support for a single or bulk upload.

-
, - )} -
-
- - - - -
- ); - } -} - -export default Form.create()(Demo); diff --git a/frontend/client/components/Editor/index.tsx b/frontend/client/components/Editor/index.tsx deleted file mode 100644 index 66221070..00000000 --- a/frontend/client/components/Editor/index.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import ReactMde, { ReactMdeTypes } from 'react-mde'; -import Showdown from 'showdown'; -import * as Styled from './styled'; -import { Input } from 'antd'; -import { Row, Col } from 'antd'; - -import { InputNumber } from 'antd'; -import Form from './Form'; - -export interface AppState { - mdeState: ReactMdeTypes.MdeState; -} - -export default class App extends React.Component<{}, AppState> { - converter: Showdown.Converter; - - constructor(props: {}) { - super(props); - this.state = { - mdeState: null, - }; - this.converter = new Showdown.Converter({ - tables: true, - simplifiedAutoLink: true, - }); - } - - handleValueChange = (mdeState: ReactMdeTypes.MdeState) => { - this.setState({ mdeState }); - }; - - onChange = () => {}; - - render() { - // https://github.com/andrerpena/react-mde - return ( -
- -
- - - Create a new proposal! - - - - - - Promise.resolve(this.converter.makeHtml(markdown)) - } - /> - - -
- ); - } -} diff --git a/frontend/client/components/Editor/styled.tsx b/frontend/client/components/Editor/styled.tsx deleted file mode 100644 index d43ec474..00000000 --- a/frontend/client/components/Editor/styled.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import styled from 'styled-components'; - -export const Header = styled.h1` - font-size: 1.5rem; -`; diff --git a/frontend/client/components/Header/styled.tsx b/frontend/client/components/Header/styled.ts similarity index 96% rename from frontend/client/components/Header/styled.tsx rename to frontend/client/components/Header/styled.ts index ab86560e..24d6a09b 100644 --- a/frontend/client/components/Header/styled.tsx +++ b/frontend/client/components/Header/styled.ts @@ -7,7 +7,7 @@ export const Placeholder = styled.div` height: ${headerHeight}; `; -export const Header = styled.header` +export const Header = styled<{ isTransparent: boolean }, 'header'>('header')` position: ${(p: any) => (p.isTransparent ? 'absolute' : 'relative')}; top: 0; left: 0; diff --git a/frontend/client/components/Home/styled.tsx b/frontend/client/components/Home/styled.ts similarity index 95% rename from frontend/client/components/Home/styled.tsx rename to frontend/client/components/Home/styled.ts index a5228ec9..47b0fb93 100644 --- a/frontend/client/components/Home/styled.tsx +++ b/frontend/client/components/Home/styled.ts @@ -41,17 +41,17 @@ export const HeroButtons = styled.div` } `; -export const HeroButton = styled.a` +export const HeroButton = styled<{ isPrimary?: boolean }, 'a'>('a')` height: 3.6rem; line-height: 3.6rem; width: 16rem; padding: 0; margin: 0 10px; - background: ${(p: any) => + background: ${p => p.isPrimary ? 'linear-gradient(-180deg, #3498DB 0%, #2C8ACA 100%)' : 'linear-gradient(-180deg, #FFFFFF 0%, #FAFAFA 98%)'}; - color: ${(p: any) => (p.isPrimary ? '#FFF' : '#4C4C4C')}; + color: ${p => (p.isPrimary ? '#FFF' : '#4C4C4C')}; text-align: center; font-size: 1.4rem; border-radius: 4px; @@ -61,7 +61,7 @@ export const HeroButton = styled.a` &:hover, &:focus { transform: translateY(-2px); - color: ${(p: any) => (p.isPrimary ? '#FFF' : '#4C4C4C')}; + color: ${p => (p.isPrimary ? '#FFF' : '#4C4C4C')}; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.25); } diff --git a/frontend/client/components/MarkdownEditor/index.tsx b/frontend/client/components/MarkdownEditor/index.tsx index 9b373d30..ac3cef2e 100644 --- a/frontend/client/components/MarkdownEditor/index.tsx +++ b/frontend/client/components/MarkdownEditor/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import ReactMde, { ReactMdeTypes, DraftUtil } from 'react-mde'; +import ReactMde, { ReactMdeTypes } from 'react-mde'; import Showdown from 'showdown'; interface Props { diff --git a/frontend/client/components/NewsletterForm/styled.tsx b/frontend/client/components/NewsletterForm/styled.tsx index 8dfe98a8..7b2c1c0f 100644 --- a/frontend/client/components/NewsletterForm/styled.tsx +++ b/frontend/client/components/NewsletterForm/styled.tsx @@ -16,7 +16,7 @@ export const Form = styled.form` } `; -export const Input = styled.input` +export const Input = styled<{ isSuccess?: boolean }, 'input'>('input')` display: block; height: ${inputHeight}; width: 100%; @@ -60,7 +60,9 @@ export const Input = styled.input` } `; -export const Button = styled.button` +export const Button = styled<{ isLoading?: boolean; isSuccess?: boolean }, 'button'>( + 'button', +)` display: block; position: absolute; top: 50%; diff --git a/frontend/client/components/Profile/index.tsx b/frontend/client/components/Profile/index.tsx deleted file mode 100644 index 7bdbd815..00000000 --- a/frontend/client/components/Profile/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import { AppState } from 'store/reducers'; -import { authActions } from 'modules/auth'; -import { getEmail } from 'modules/auth/selectors'; -import { connect } from 'react-redux'; -import { compose } from 'recompose'; -import { bindActionCreators, Dispatch } from 'redux'; -import { Button } from 'antd'; - -interface StateProps { - email: string | null; -} - -interface DispatchProps { - logoutAndRedirect: authActions.TLogoutAndRedirect; -} - -type Props = DispatchProps & StateProps; - -class Profile extends React.Component { - render() { - return ( -
- - -

hi profile. {this.props.email}

-
- ); - } -} - -function mapStateToProps(state: AppState) { - return { - email: getEmail(state), - }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return bindActionCreators(authActions, dispatch); -} - -const withConnect = connect( - mapStateToProps, - mapDispatchToProps, -); - -export default compose(withConnect)(Profile); diff --git a/frontend/client/components/Proposal/CampaignBlock/index.tsx b/frontend/client/components/Proposal/CampaignBlock/index.tsx index 71100567..4aa959ad 100644 --- a/frontend/client/components/Proposal/CampaignBlock/index.tsx +++ b/frontend/client/components/Proposal/CampaignBlock/index.tsx @@ -203,7 +203,10 @@ const withConnect = connect( { fundCrowdFund: web3Actions.fundCrowdFund }, ); -const ConnectedCampaignBlock = withRouter(compose(withConnect)(CampaignBlock)); +const ConnectedCampaignBlock = compose( + withRouter, + withConnect, +)(CampaignBlock); export default (props: OwnProps) => ( ( )} - render={({ web3, accounts, contracts }) => ( - - )} + render={() => } /> ); diff --git a/frontend/client/components/Proposal/CampaignBlock/styled.ts b/frontend/client/components/Proposal/CampaignBlock/styled.ts index e4e429c5..c3818ffd 100644 --- a/frontend/client/components/Proposal/CampaignBlock/styled.ts +++ b/frontend/client/components/Proposal/CampaignBlock/styled.ts @@ -64,13 +64,13 @@ export const Button = styled.a` } `; -export const FundingOverMessage = styled.div` +export const FundingOverMessage = styled<{ isSuccess: boolean }, 'div'>('div')` display: flex; justify-content: center; align-items: center; margin: 0.5rem -1rem 0; font-size: 1.15rem; - color: ${(p: any) => (p.isSuccess ? '#2ECC71' : '#E74C3C')}; + color: ${p => (p.isSuccess ? '#2ECC71' : '#E74C3C')}; .anticon { font-size: 1.5rem; diff --git a/frontend/client/components/Proposal/Milestones/index.tsx b/frontend/client/components/Proposal/Milestones/index.tsx index 438a6ce8..b8a7ea24 100644 --- a/frontend/client/components/Proposal/Milestones/index.tsx +++ b/frontend/client/components/Proposal/Milestones/index.tsx @@ -1,11 +1,7 @@ import React from 'react'; import moment from 'moment'; import { Timeline, Spin, Icon } from 'antd'; -import { - ProposalWithCrowdFund, - Milestone, - MILESTONE_STATE, -} from 'modules/proposals/reducers'; +import { ProposalWithCrowdFund, MILESTONE_STATE } from 'modules/proposals/reducers'; import Web3Container, { Web3RenderProps } from 'lib/Web3Container'; import * as Styled from './styled'; @@ -14,7 +10,7 @@ interface OwnProps { } interface Web3Props { - web3: any; + web3: Web3RenderProps['web3']; } type Props = OwnProps & Web3Props; diff --git a/frontend/client/components/Proposal/index.tsx b/frontend/client/components/Proposal/index.tsx index 5d17c11a..f53a2a00 100644 --- a/frontend/client/components/Proposal/index.tsx +++ b/frontend/client/components/Proposal/index.tsx @@ -126,12 +126,15 @@ function mapDispatchToProps(dispatch: Dispatch) { return bindActionCreators({ ...proposalActions, ...web3Actions }, dispatch); } -const withConnect = connect( +const withConnect = connect( mapStateToProps, mapDispatchToProps, ); -const ConnectedProposal = withRouter(compose(withConnect)(ProposalDetail)); +const ConnectedProposal = compose( + withRouter, + withConnect, +)(ProposalDetail); export default (props: OwnProps) => ( ( )} - render={({ web3, accounts, contracts }) => ( - - )} + render={() => } /> ); diff --git a/frontend/client/components/Proposal/styled.tsx b/frontend/client/components/Proposal/styled.tsx index 04c05f8c..d2c86dce 100644 --- a/frontend/client/components/Proposal/styled.tsx +++ b/frontend/client/components/Proposal/styled.tsx @@ -110,7 +110,7 @@ export const SideBlock = styled.div` } `; -export const BodyText = styled.div` +export const BodyText = styled<{ isExpanded: boolean }, 'div'>('div')` max-height: ${(p: any) => (p.isExpanded ? 'none' : '27rem')}; overflow: hidden; font-size: 1.1rem; diff --git a/frontend/client/components/Proposals/Filters/index.tsx b/frontend/client/components/Proposals/Filters/index.tsx index eee507f1..821ad31a 100644 --- a/frontend/client/components/Proposals/Filters/index.tsx +++ b/frontend/client/components/Proposals/Filters/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Select, Checkbox, Radio, Card, Divider, Affix } from 'antd'; +import { RadioChangeEvent } from 'antd/lib/radio'; import { PROPOSAL_SORT, SORT_LABELS, @@ -73,7 +74,7 @@ export default class ProposalFilters extends React.Component { ); } - private handleCategoryChange = (ev: React.ChangeEvent) => { + private handleCategoryChange = (ev: RadioChangeEvent) => { const { filters } = this.props; const category = ev.target.value as PROPOSAL_CATEGORY; const categories = ev.target.checked @@ -86,7 +87,7 @@ export default class ProposalFilters extends React.Component { }); }; - private handleStageChange = (ev: React.ChangeEvent) => { + private handleStageChange = (ev: RadioChangeEvent) => { this.props.handleChangeFilters({ ...this.props.filters, stage: ev.target.value as PROPOSAL_STAGE, diff --git a/frontend/client/components/Proposals/Filters/styled.ts b/frontend/client/components/Proposals/Filters/styled.ts deleted file mode 100644 index 4f210a90..00000000 --- a/frontend/client/components/Proposals/Filters/styled.ts +++ /dev/null @@ -1 +0,0 @@ -import styled from 'styled-components'; diff --git a/frontend/client/components/Proposals/ProposalCard/styled.ts b/frontend/client/components/Proposals/ProposalCard/styled.ts index cf70f698..2dd8eef3 100644 --- a/frontend/client/components/Proposals/ProposalCard/styled.ts +++ b/frontend/client/components/Proposals/ProposalCard/styled.ts @@ -80,8 +80,8 @@ export const FundingRaised = styled.div` } `; -export const FundingPercent = styled.div` - color: ${(p: any) => (p.isFunded ? '#2ecc71' : 'inherit')}; +export const FundingPercent = styled<{ isFunded: boolean }, 'div'>('div')` + color: ${p => (p.isFunded ? '#2ecc71' : 'inherit')}; font-size: 0.7rem; padding-left: 0.25rem; `; diff --git a/frontend/client/components/Proposals/index.tsx b/frontend/client/components/Proposals/index.tsx index d07a3fcd..3b5b0f54 100644 --- a/frontend/client/components/Proposals/index.tsx +++ b/frontend/client/components/Proposals/index.tsx @@ -49,6 +49,8 @@ const sortFunctions: { [key in PROPOSAL_SORT]: ProposalSortFn } = { interface StateProps { proposals: ReturnType; + proposalsError: AppState['proposal']['proposalsError']; + isFetchingProposals: AppState['proposal']['isFetchingProposals']; } interface DispatchProps { @@ -185,16 +187,6 @@ const withConnect = connect( const ConnectedProposals = compose(withConnect)(Proposals); -export default props => ( - } - render={({ web3, accounts, contracts }) => ( - - )} - /> +export default () => ( + } render={() => } /> ); diff --git a/frontend/client/lib/Web3Container.tsx b/frontend/client/lib/Web3Container.tsx index dfe61f0b..9022a81b 100644 --- a/frontend/client/lib/Web3Container.tsx +++ b/frontend/client/lib/Web3Container.tsx @@ -1,9 +1,10 @@ import React from 'react'; -const CrowdFundFactory = require('./contracts/CrowdFundFactory.json'); import { bindActionCreators, Dispatch } from 'redux'; import { connect } from 'react-redux'; import { AppState } from 'store/reducers'; import { web3Actions } from 'modules/web3'; +/* tslint:disable no-var-requires --- TODO: find a better way to import json */ +const CrowdFundFactory = require('./contracts/CrowdFundFactory.json'); export interface Web3RenderProps { web3: any; @@ -28,9 +29,9 @@ interface StateProps { } interface ActionProps { - setContract(contract: any): void; - setAccounts(): void; - setWeb3(): void; + setContract: typeof web3Actions['setContract']; + setAccounts: typeof web3Actions['setAccounts']; + setWeb3: typeof web3Actions['setWeb3']; } type Props = OwnProps & StateProps & ActionProps; diff --git a/frontend/client/lib/getWeb3.ts b/frontend/client/lib/getWeb3.ts index e1cf786f..a8f75eb7 100644 --- a/frontend/client/lib/getWeb3.ts +++ b/frontend/client/lib/getWeb3.ts @@ -1,7 +1,15 @@ import Web3 from 'web3'; -const resolveWeb3 = (resolve, reject) => { - let { web3 } = window; +interface Web3Window extends Window { + web3?: Web3; +} + +const resolveWeb3 = (resolve: (web3: Web3) => void, reject: (err: Error) => void) => { + if (typeof window === 'undefined') { + return reject(new Error('No global window variable')); + } + + let { web3 } = window as Web3Window; const alreadyInjected = typeof web3 !== 'undefined'; // i.e. Mist/Metamask const localProvider = `http://localhost:8545`; diff --git a/frontend/client/lib/with-redux-store.tsx b/frontend/client/lib/with-redux-store.tsx index 5278d903..3773fbac 100644 --- a/frontend/client/lib/with-redux-store.tsx +++ b/frontend/client/lib/with-redux-store.tsx @@ -1,25 +1,31 @@ import React from 'react'; +import { AppState } from 'store/reducers'; import { configureStore } from 'store/configure'; const isServer = typeof window === 'undefined'; const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__'; -function getOrCreateStore(initialState?: any) { +function getOrCreateStore(initialState?: Partial) { // Always make a new store if server, otherwise state is shared between requests if (isServer) { return configureStore(initialState); } // Create store if unavailable on the client and set it on the window object - if (!window[__NEXT_REDUX_STORE__]) { - window[__NEXT_REDUX_STORE__] = configureStore(initialState); + const anyWindow = window as any; + if (!anyWindow[__NEXT_REDUX_STORE__]) { + anyWindow[__NEXT_REDUX_STORE__] = configureStore(initialState); } - return window[__NEXT_REDUX_STORE__]; + return anyWindow[__NEXT_REDUX_STORE__]; } -export default App => { - return class AppWithRedux extends React.Component { - static async getInitialProps(appContext) { +interface Props { + initialReduxState: Partial; +} + +export default (App: any) => { + return class AppWithRedux extends React.Component { + static async getInitialProps(appContext: any) { // Get or Create the store with `undefined` as INITIAL_STATE // This allows you to set a custom default INITIAL_STATE const store = getOrCreateStore(); @@ -38,7 +44,8 @@ export default App => { }; } - constructor(props) { + private store: any; + constructor(props: Props) { super(props); this.store = getOrCreateStore(props.initialReduxState); } diff --git a/frontend/client/modules/proposals/selectors.tsx b/frontend/client/modules/proposals/selectors.tsx index a7efd945..39503417 100644 --- a/frontend/client/modules/proposals/selectors.tsx +++ b/frontend/client/modules/proposals/selectors.tsx @@ -16,14 +16,6 @@ export function getProposal( ); } -export function getProposalForumURL( - state: AppState, - proposalId: ProposalWithCrowdFund['proposalId'], -): string | null { - const proposal = getProposal(state, proposalId); - return proposal ? proposal.forum_url : null; -} - export function getProposalComments( state: AppState, proposalId: ProposalWithCrowdFund['proposalId'], diff --git a/frontend/client/modules/web3/actions.ts b/frontend/client/modules/web3/actions.ts index 3f780665..6ab7284a 100644 --- a/frontend/client/modules/web3/actions.ts +++ b/frontend/client/modules/web3/actions.ts @@ -5,6 +5,10 @@ import { postProposal } from 'api/api'; import getContract, { WrongNetworkError } from 'lib/getContract'; import { sleep } from 'utils/helpers'; import { fetchProposal, fetchProposals } from 'modules/proposals/actions'; +import { PROPOSAL_CATEGORY } from 'api/constants'; +import { AppState } from 'store/reducers'; + +type GetState = () => AppState; function handleWrongNetworkError(dispatch: (action: any) => void) { return (err: Error) => { @@ -27,11 +31,12 @@ export function setWeb3() { } export type TSetContract = typeof setContract; -export function setContract(json, deployedAddress?) { - return (dispatch: Dispatch, getState: any) => { +export function setContract(json: any, deployedAddress?: string) { + return (dispatch: Dispatch, getState: GetState) => { const state = getState(); if (state.web3.web3) { - dispatch({ + // TODO: Type me as promise dispatch + (dispatch as any)({ type: types.CONTRACT, payload: getContract(state.web3.web3, json, deployedAddress), }).catch(handleWrongNetworkError(dispatch)); @@ -48,7 +53,7 @@ export function setContract(json, deployedAddress?) { export type TSetAccounts = typeof setAccounts; export function setAccounts() { - return (dispatch: Dispatch, getState: any) => { + return (dispatch: Dispatch, getState: GetState) => { const state = getState(); if (state.web3.web3) { dispatch({ type: types.ACCOUNTS_PENDING }); @@ -82,9 +87,39 @@ export function setAccounts() { }; } +// TODO: Move these to a better place? +interface MilestoneData { + title: string; + description: string; + date: string; + payoutPercent: number; + immediatePayout: boolean; +} + +interface ProposalContractData { + ethAmount: number | string; // TODO: BigNumber + payOutAddress: string; + trusteesAddresses: string[]; + milestoneAmounts: number[] | string[]; // TODO: BigNumber + milestones: MilestoneData[]; + durationInMinutes: number; + milestoneVotingPeriodInMinutes: number; + immediateFirstMilestonePayout: boolean; +} + +interface ProposalBackendData { + title: string; + content: string; + category: PROPOSAL_CATEGORY; +} + export type TCreateCrowdFund = typeof createCrowdFund; -export function createCrowdFund(CrowdFundFactoryContract, contractData, backendData) { - return async (dispatch: Dispatch, getState) => { +export function createCrowdFund( + CrowdFundFactoryContract: any, + contractData: ProposalContractData, + backendData: ProposalBackendData, +) { + return async (dispatch: Dispatch, getState: GetState) => { dispatch({ type: types.CROWD_FUND_PENDING, }); @@ -117,7 +152,7 @@ export function createCrowdFund(CrowdFundFactoryContract, contractData, backendD immediateFirstMilestonePayout, ) .send({ from: accounts[0] }) - .once('confirmation', async function(confNumber, receipt) { + .once('confirmation', async (_: any, receipt: any) => { const crowdFundContractAddress = receipt.events.ContractCreated.returnValues.newAddress; await postProposal({ @@ -132,7 +167,8 @@ export function createCrowdFund(CrowdFundFactoryContract, contractData, backendD type: types.CROWD_FUND_CREATED, payload: crowdFundContractAddress, }); - dispatch(fetchProposals()).catch(handleWrongNetworkError(dispatch)); + // TODO: Type me as promise dispatch + (dispatch as any)(fetchProposals()).catch(handleWrongNetworkError(dispatch)); }); } catch (err) { dispatch({ @@ -145,8 +181,8 @@ export function createCrowdFund(CrowdFundFactoryContract, contractData, backendD } export type TRequestMilestonePayout = typeof requestMilestonePayout; -export function requestMilestonePayout(crowdFundContract, index) { - return async (dispatch: Dispatch, getState) => { +export function requestMilestonePayout(crowdFundContract: any, index: number) { + return async (dispatch: Dispatch, getState: GetState) => { dispatch({ type: types.REQUEST_MILESTONE_PAYOUT_PENDING, }); @@ -156,8 +192,7 @@ export function requestMilestonePayout(crowdFundContract, index) { await crowdFundContract.methods .requestMilestonePayout(index) .send({ from: account }) - .once('confirmation', async function(confNumber, receipt) { - console.info('Milestone payout request confirmed', { confNumber, receipt }); + .once('confirmation', async () => { await sleep(5000); await dispatch(fetchProposal(crowdFundContract._address)); dispatch({ @@ -175,8 +210,8 @@ export function requestMilestonePayout(crowdFundContract, index) { } export type TPayMilestonePayout = typeof payMilestonePayout; -export function payMilestonePayout(crowdFundContract, index) { - return async (dispatch: Dispatch, getState) => { +export function payMilestonePayout(crowdFundContract: any, index: number) { + return async (dispatch: Dispatch, getState: GetState) => { dispatch({ type: types.PAY_MILESTONE_PAYOUT_PENDING, }); @@ -186,7 +221,7 @@ export function payMilestonePayout(crowdFundContract, index) { await crowdFundContract.methods .payMilestonePayout(index) .send({ from: account }) - .once('confirmation', async function(confNumber, receipt) { + .once('confirmation', async () => { await sleep(5000); await dispatch(fetchProposal(crowdFundContract._address)); dispatch({ @@ -204,9 +239,10 @@ export function payMilestonePayout(crowdFundContract, index) { }; } +// TODO: BigNumber me export type TSendTransaction = typeof fundCrowdFund; -export function fundCrowdFund(crowdFundContract, value) { - return async (dispatch: Dispatch, getState) => { +export function fundCrowdFund(crowdFundContract: any, value: number | string) { + return async (dispatch: Dispatch, getState: GetState) => { dispatch({ type: types.SEND_PENDING, }); @@ -218,7 +254,7 @@ export function fundCrowdFund(crowdFundContract, value) { await crowdFundContract.methods .contribute() .send({ from: account, value: web3.utils.toWei(String(value), 'ether') }) - .once('confirmation', async function(confNumber, receipt) { + .once('confirmation', async () => { await sleep(5000); await dispatch(fetchProposal(crowdFundContract._address)); dispatch({ @@ -241,7 +277,7 @@ export function voteMilestonePayout( index: number, vote: boolean, ) { - return async (dispatch: Dispatch, getState: any) => { + return async (dispatch: Dispatch, getState: GetState) => { dispatch({ type: types.VOTE_AGAINST_MILESTONE_PAYOUT_PENDING }); const state = getState(); const account = state.web3.accounts[0]; diff --git a/frontend/client/modules/web3/selectors.ts b/frontend/client/modules/web3/selectors.ts index c6e8bc28..a67278d5 100644 --- a/frontend/client/modules/web3/selectors.ts +++ b/frontend/client/modules/web3/selectors.ts @@ -1,8 +1,6 @@ import { AppState } from 'store/reducers'; -export function findCrowdFund(state: AppState, contractAddress) { - const { crowdFunds } = state.web3; - return crowdFunds.find(crowdFund => { - return crowdFund.crowdFundContract._address === contractAddress; - }); +export function findContract(state: AppState, contractAddress: string) { + const { contracts } = state.web3; + return contracts.find(contract => contract._address === contractAddress); } diff --git a/frontend/client/pages/proposal.tsx b/frontend/client/pages/proposal.tsx index 1b5146fd..74a8a7bd 100644 --- a/frontend/client/pages/proposal.tsx +++ b/frontend/client/pages/proposal.tsx @@ -11,7 +11,7 @@ class ProposalPage extends Component { super(props); } render() { - const proposalId = this.props.router.query.id; + const proposalId = this.props.router.query.id as string; return ( { return applyMiddleware(...middleware); }; -export function configureStore(initialState = combineInitialState): Store { - const store: any = createStore( +export function configureStore( + initialState: Partial = combineInitialState, +): Store { + const store: Store = createStore( rootReducer, initialState, bindMiddleware([sagaMiddleware, thunkMiddleware, promiseMiddleware()]), diff --git a/frontend/client/utils/cookie.ts b/frontend/client/utils/cookie.ts index f4f43901..d94d3c47 100644 --- a/frontend/client/utils/cookie.ts +++ b/frontend/client/utils/cookie.ts @@ -2,8 +2,9 @@ // https://github.com/carlos-peru/next-with-api/blob/master/lib/session.js import cookie from 'js-cookie'; +import { AxiosRequestConfig } from 'axios'; -export const setCookie = (key, value) => { +export const setCookie = (key: string, value: string) => { if (process.browser) { cookie.set(key, value, { expires: 1, @@ -12,7 +13,7 @@ export const setCookie = (key, value) => { } }; -export const removeCookie = key => { +export const removeCookie = (key: string) => { if (process.browser) { cookie.remove(key, { expires: 1, @@ -20,21 +21,21 @@ export const removeCookie = key => { } }; -export const getCookie = (key, req) => { +export const getCookie = (key: string, req: AxiosRequestConfig) => { return process.browser ? getCookieFromBrowser(key) : getCookieFromServer(key, req); }; -const getCookieFromBrowser = key => { +const getCookieFromBrowser = (key: string) => { return cookie.get(key); }; -const getCookieFromServer = (key, req) => { +const getCookieFromServer = (key: string, req: AxiosRequestConfig) => { if (!req.headers.cookie) { return undefined; } const rawCookie = req.headers.cookie .split(';') - .find(c => c.trim().startsWith(`${key}=`)); + .find((c: string) => c.trim().startsWith(`${key}=`)); if (!rawCookie) { return undefined; } diff --git a/frontend/client/utils/helpers.ts b/frontend/client/utils/helpers.ts index 6de4f666..6921b17e 100644 --- a/frontend/client/utils/helpers.ts +++ b/frontend/client/utils/helpers.ts @@ -1,5 +1,3 @@ -import { Milestone } from 'modules/proposals/reducers'; - export function isNumeric(n: any) { return !isNaN(parseFloat(n)) && isFinite(n); } diff --git a/frontend/client/utils/web3Utils.ts b/frontend/client/utils/web3Utils.ts index cbd8e0f4..d4885670 100644 --- a/frontend/client/utils/web3Utils.ts +++ b/frontend/client/utils/web3Utils.ts @@ -1,4 +1,11 @@ -export async function collectArrayElements(method, account) { +import { TransactionObject } from 'web3/eth/types'; + +type Web3Method = (index: number) => TransactionObject; + +export async function collectArrayElements( + method: Web3Method, + account: string, +): Promise { const arrayElements = []; let noError = true; let index = 0; diff --git a/frontend/client/web3interact/crowdFund.ts b/frontend/client/web3interact/crowdFund.ts index 30ab5b56..f410a60d 100644 --- a/frontend/client/web3interact/crowdFund.ts +++ b/frontend/client/web3interact/crowdFund.ts @@ -22,7 +22,7 @@ export async function getCrowdFundState( : await web3.eth.getBalance(crowdFundContract._address); const isFrozen = await crowdFundContract.methods.frozen().call({ from: account }); - const trustees = await collectArrayElements( + const trustees = await collectArrayElements( crowdFundContract.methods.trustees, account, ); diff --git a/frontend/package.json b/frontend/package.json index 9d014697..09e6571d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,8 @@ }, "husky": { "hooks": { - "pre-commit": "lint-staged" + "pre-commit": "lint-staged", + "pre-push": "yarn run lint && yarn run tsc" } }, "lint-staged": { @@ -121,6 +122,7 @@ "devDependencies": { "@types/bn.js": "4.11.1", "@types/showdown": "1.7.5", + "@types/web3": "1.0.3", "rimraf": "2.6.2" } } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 71948d56..9a06c963 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -747,7 +747,7 @@ dependencies: any-observable "^0.3.0" -"@types/bn.js@4.11.1": +"@types/bn.js@*", "@types/bn.js@4.11.1": version "4.11.1" resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.1.tgz#6fd07b93490ecf0f3501a31ea9cfd330885b10fa" dependencies: @@ -853,6 +853,17 @@ version "1.7.5" resolved "https://registry.yarnpkg.com/@types/showdown/-/showdown-1.7.5.tgz#91061f2f16d5bdf66b186185999ed675a8908b6a" +"@types/underscore@*": + version "1.8.9" + resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.8.9.tgz#fef41f800cd23db1b4f262ddefe49cd952d82323" + +"@types/web3@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/web3/-/web3-1.0.3.tgz#2c5f6905d46eb6e40da8eb7c1e553463862fa599" + dependencies: + "@types/bn.js" "*" + "@types/underscore" "*" + "@zeit/next-css@0.2.0", "@zeit/next-css@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@zeit/next-css/-/next-css-0.2.0.tgz#35da071256397b509b86ac7726ce0f7d3593e62b"