Working Refunds (#32)
* Add and use placeholder component. * Allow debugging from truffle * Implement refunding * Fix tsc * Double transaction for first refund.
This commit is contained in:
parent
ab66cf6ea4
commit
df1160acf5
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import * as Styled from './styled';
|
||||
|
||||
interface Props {
|
||||
title?: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const Placeholder: React.SFC<Props> = ({ style = {}, title, subtitle }) => (
|
||||
<Styled.Container style={style}>
|
||||
{title && <Styled.Title>{title}</Styled.Title>}
|
||||
{subtitle && <Styled.Subtitle>{subtitle}</Styled.Subtitle>}
|
||||
</Styled.Container>
|
||||
);
|
||||
|
||||
export default Placeholder;
|
|
@ -0,0 +1,26 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 2px dashed #d9d9d9;
|
||||
padding: 3rem;
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
export const Title = styled.h3`
|
||||
margin-bottom: 0;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
font-size: 1.6rem;
|
||||
|
||||
& + div {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Subtitle = styled.div`
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
font-size: 1rem;
|
||||
`;
|
|
@ -187,30 +187,15 @@ class Milestones extends React.Component<Props> {
|
|||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{showVoteProgress && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
margin: '0 2rem 0.5rem 0',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Styled.ProgressContainer>
|
||||
<Progress
|
||||
type="dashboard"
|
||||
percent={activeVoteMilestone.percentAgainstPayout}
|
||||
format={p => `${p}%`}
|
||||
status="exception"
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
opacity: 0.6,
|
||||
fontSize: '0.75rem',
|
||||
}}
|
||||
>
|
||||
voted against payout
|
||||
</div>
|
||||
</div>
|
||||
<Styled.ProgressText>voted against payout</Styled.ProgressText>
|
||||
</Styled.ProgressContainer>
|
||||
)}
|
||||
<div>
|
||||
{content}
|
||||
|
|
|
@ -1,56 +1,182 @@
|
|||
import React from 'react';
|
||||
import { Spin, Progress, Button } from 'antd';
|
||||
import { connect } from 'react-redux';
|
||||
import { Spin, Progress, Button, Alert } from 'antd';
|
||||
import { ProposalWithCrowdFund } from 'modules/proposals/reducers';
|
||||
import Web3Container, { Web3RenderProps } from 'lib/Web3Container';
|
||||
import { web3Actions } from 'modules/web3';
|
||||
import { AppState } from 'store/reducers';
|
||||
import * as Styled from './styled';
|
||||
|
||||
interface OwnProps {
|
||||
proposal: ProposalWithCrowdFund;
|
||||
}
|
||||
|
||||
interface Web3Props {
|
||||
web3: Web3RenderProps['web3'];
|
||||
interface StateProps {
|
||||
isRefundActionPending: AppState['web3']['isRefundActionPending'];
|
||||
refundActionError: AppState['web3']['refundActionError'];
|
||||
}
|
||||
|
||||
type Props = OwnProps & Web3Props;
|
||||
interface ActionProps {
|
||||
voteRefund: typeof web3Actions['voteRefund'];
|
||||
withdrawRefund: typeof web3Actions['withdrawRefund'];
|
||||
}
|
||||
|
||||
interface Web3Props {
|
||||
web3: Web3RenderProps['web3'];
|
||||
account: Web3RenderProps['accounts'][0];
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps & ActionProps & Web3Props;
|
||||
|
||||
class GovernanceRefunds extends React.Component<Props> {
|
||||
render() {
|
||||
const fundPct = 32;
|
||||
const { proposal, account, isRefundActionPending, refundActionError } = this.props;
|
||||
const { crowdFund } = proposal;
|
||||
const contributor = crowdFund.contributors.find(c => c.address === account);
|
||||
const isTrustee = crowdFund.trustees.includes(account);
|
||||
const hasVotedForRefund = contributor && contributor.refundVote;
|
||||
const hasRefunded = contributor && contributor.refunded;
|
||||
const refundPct = Math.floor(
|
||||
(crowdFund.amountVotingForRefund / crowdFund.target) * 100,
|
||||
);
|
||||
const color = refundPct < 10 ? '#1890ff' : refundPct < 50 ? '#faad14' : '#f5222d';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'center', marginRight: '2rem' }}>
|
||||
<Progress
|
||||
type="dashboard"
|
||||
percent={fundPct}
|
||||
format={p => `${p}%`}
|
||||
status="exception"
|
||||
/>
|
||||
<p style={{ opacity: 0.6, fontSize: '0.75rem' }}>voted for a refund</p>
|
||||
</div>
|
||||
<div>
|
||||
<p style={{ fontSize: '1rem' }}>
|
||||
let text;
|
||||
let button;
|
||||
if (!isTrustee && contributor) {
|
||||
if (refundPct < 50) {
|
||||
text = `
|
||||
As a funder of this project, you have the right to vote for a refund. If the
|
||||
amount of funds contributed by refund voters exceeds half of the project's
|
||||
total raised funds, a refund will be issued to everyone.
|
||||
</p>
|
||||
<Button type="danger" block>
|
||||
Vote for a Refund
|
||||
total raised funds, all funders will be able to request refunds.
|
||||
`;
|
||||
if (hasVotedForRefund) {
|
||||
button = {
|
||||
text: 'Undo vote for refund',
|
||||
type: 'danger',
|
||||
onClick: () => this.voteRefund(false),
|
||||
};
|
||||
} else {
|
||||
button = {
|
||||
text: 'Vote for refund',
|
||||
type: 'danger',
|
||||
onClick: () => this.voteRefund(true),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (hasRefunded) {
|
||||
return (
|
||||
<Alert
|
||||
type="success"
|
||||
message="Your refund has been processed"
|
||||
description={`
|
||||
We apologize for any inconvenience this propsal has caused you. Please
|
||||
let us know if there's anything we could have done to improve your
|
||||
experience.
|
||||
`}
|
||||
showIcon
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
text = (
|
||||
<>
|
||||
The majority of funders have voted for a refund. Click below to receive your
|
||||
refund.
|
||||
{!crowdFund.isFrozen && (
|
||||
<Alert
|
||||
style={{ marginTop: '1rem' }}
|
||||
type="info"
|
||||
message={`
|
||||
This will require multiple transactions to process, sorry
|
||||
for the inconvenience
|
||||
`}
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
button = {
|
||||
text: 'Get your refund',
|
||||
type: 'primary',
|
||||
onClick: () => this.withdrawRefund(),
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (refundPct < 50) {
|
||||
text = `
|
||||
Funders can vote to request refunds. If the amount of funds contributed by
|
||||
refund voters exceeds half of the funds contributed, all funders will be able
|
||||
to request refunds.
|
||||
`;
|
||||
} else {
|
||||
text = `
|
||||
The funders of this project have voted for a refund. All funders can request refunds,
|
||||
and the project will no longer receive any payouts.
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Styled.ProgressContainer stroke={color}>
|
||||
<Progress type="dashboard" percent={refundPct} format={p => `${p}%`} />
|
||||
<Styled.ProgressText>voted for a refund</Styled.ProgressText>
|
||||
</Styled.ProgressContainer>
|
||||
<div>
|
||||
<p style={{ fontSize: '1rem' }}>{text}</p>
|
||||
{button && (
|
||||
<Button
|
||||
type={button.type as any}
|
||||
onClick={button.onClick}
|
||||
loading={isRefundActionPending}
|
||||
block
|
||||
>
|
||||
{button.text}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{refundActionError && (
|
||||
<Alert
|
||||
type="error"
|
||||
message="Something went wrong!"
|
||||
description={refundActionError}
|
||||
style={{ margin: '1rem 0 0' }}
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
voteRefund = (vote: boolean) => {
|
||||
this.props.voteRefund(this.props.proposal.crowdFundContract, vote);
|
||||
};
|
||||
|
||||
withdrawRefund = () => {
|
||||
const { proposal, account } = this.props;
|
||||
this.props.withdrawRefund(proposal.crowdFundContract, account);
|
||||
};
|
||||
}
|
||||
|
||||
const ConnectedGovernanceRefunds = connect<StateProps, ActionProps, OwnProps, AppState>(
|
||||
state => ({
|
||||
isRefundActionPending: state.web3.isRefundActionPending,
|
||||
refundActionError: state.web3.refundActionError,
|
||||
}),
|
||||
{
|
||||
voteRefund: web3Actions.voteRefund,
|
||||
withdrawRefund: web3Actions.withdrawRefund,
|
||||
},
|
||||
)(GovernanceRefunds);
|
||||
|
||||
export default (props: OwnProps) => (
|
||||
<Web3Container
|
||||
renderLoading={() => <Spin />}
|
||||
render={({ web3 }: Web3RenderProps) => <GovernanceRefunds web3={web3} {...props} />}
|
||||
render={({ web3, accounts }: Web3RenderProps) => (
|
||||
<ConnectedGovernanceRefunds web3={web3} account={accounts[0]} {...props} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import GovernanceMilestones from './Milestones';
|
||||
import GovernanceRefunds from './Refunds';
|
||||
import { ProposalWithCrowdFund } from 'modules/proposals/reducers';
|
||||
import Placeholder from 'components/Placeholder';
|
||||
import * as Styled from './styled';
|
||||
|
||||
interface Props {
|
||||
|
@ -14,10 +15,14 @@ export default class ProposalGovernance extends React.Component<Props> {
|
|||
|
||||
if (!proposal.crowdFund.isRaiseGoalReached) {
|
||||
return (
|
||||
<p>
|
||||
Milestone history and voting will be displayed here once the project has been
|
||||
funded.
|
||||
</p>
|
||||
<Placeholder
|
||||
style={{ minHeight: '220px' }}
|
||||
title="Governance isn’t available yet"
|
||||
subtitle={`
|
||||
Milestone history and voting will be displayed here once the
|
||||
project has been funded
|
||||
`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -28,3 +28,22 @@ export const GovernanceDivider = styled.div`
|
|||
export const MilestoneActionText = styled.p`
|
||||
font-size: 1rem;
|
||||
`;
|
||||
|
||||
// Shared
|
||||
export const ProgressContainer = styled<{ stroke?: string }, 'div'>('div')`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 0 2rem 0.5rem 0;
|
||||
|
||||
.ant-progress-circle-path {
|
||||
stroke: ${p => p.stroke || 'inherit'};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ProgressText = styled.div`
|
||||
white-space: nowrap;
|
||||
opacity: 0.6;
|
||||
font-size: 0.75rem;
|
||||
`;
|
||||
|
|
|
@ -16,6 +16,7 @@ export interface Contributor {
|
|||
address: string;
|
||||
contributionAmount: string;
|
||||
refundVote: boolean;
|
||||
refunded: boolean;
|
||||
proportionalContribution: string;
|
||||
milestoneNoVotes: boolean[];
|
||||
}
|
||||
|
@ -52,8 +53,10 @@ export interface ProposalMilestone extends Milestone {
|
|||
|
||||
export interface CrowdFund {
|
||||
immediateFirstMilestonePayout: boolean;
|
||||
balance: number;
|
||||
funded: number;
|
||||
target: number;
|
||||
amountVotingForRefund: number;
|
||||
beneficiary: string;
|
||||
deadline: number;
|
||||
trustees: string[];
|
||||
|
|
|
@ -301,3 +301,71 @@ export function voteMilestonePayout(
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function voteRefund(crowdFundContract: any, vote: boolean) {
|
||||
return async (dispatch: Dispatch<any>, getState: GetState) => {
|
||||
dispatch({ type: types.VOTE_REFUND_PENDING });
|
||||
const state = getState();
|
||||
const account = state.web3.accounts[0];
|
||||
|
||||
try {
|
||||
await crowdFundContract.methods
|
||||
.voteRefund(vote)
|
||||
.send({ from: account })
|
||||
.once('confirmation', async () => {
|
||||
await sleep(5000);
|
||||
await dispatch(fetchProposal(crowdFundContract._address));
|
||||
dispatch({ type: types.VOTE_REFUND_FULFILLED });
|
||||
});
|
||||
} catch (err) {
|
||||
dispatch({
|
||||
type: types.VOTE_REFUND_REJECTED,
|
||||
payload: err.message || err.toString(),
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function withdrawRefund(crowdFundContract: any, address: string) {
|
||||
return async (dispatch: Dispatch<any>, getState: GetState) => {
|
||||
dispatch({ type: types.WITHDRAW_REFUND_PENDING });
|
||||
const state = getState();
|
||||
const account = state.web3.accounts[0];
|
||||
|
||||
try {
|
||||
let isFrozen = await crowdFundContract.methods.frozen().call({ from: account });
|
||||
if (!isFrozen) {
|
||||
await new Promise(resolve => {
|
||||
crowdFundContract.methods
|
||||
.refund()
|
||||
.send({ from: account })
|
||||
.once('confirmation', async () => {
|
||||
await sleep(5000);
|
||||
isFrozen = await crowdFundContract.methods.frozen().call({ from: account });
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!isFrozen) {
|
||||
throw new Error('Proposal isn’t in a refundable state yet.');
|
||||
}
|
||||
|
||||
await crowdFundContract.methods
|
||||
.withdraw(address)
|
||||
.send({ from: account })
|
||||
.once('confirmation', async () => {
|
||||
await sleep(5000);
|
||||
await dispatch(fetchProposal(crowdFundContract._address));
|
||||
dispatch({ type: types.WITHDRAW_REFUND_FULFILLED });
|
||||
});
|
||||
} catch (err) {
|
||||
dispatch({
|
||||
type: types.WITHDRAW_REFUND_REJECTED,
|
||||
payload: err.message || err.toString(),
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -27,6 +27,9 @@ export interface Web3State {
|
|||
|
||||
isMilestoneActionPending: boolean;
|
||||
milestoneActionError: null | string;
|
||||
|
||||
isRefundActionPending: boolean;
|
||||
refundActionError: null | string;
|
||||
}
|
||||
|
||||
export const INITIAL_STATE: Web3State = {
|
||||
|
@ -52,6 +55,9 @@ export const INITIAL_STATE: Web3State = {
|
|||
|
||||
isMilestoneActionPending: false,
|
||||
milestoneActionError: null,
|
||||
|
||||
isRefundActionPending: false,
|
||||
refundActionError: null,
|
||||
};
|
||||
|
||||
function addContract(state: Web3State, payload: Contract) {
|
||||
|
@ -198,6 +204,27 @@ export default (state = INITIAL_STATE, action: any): Web3State => {
|
|||
isMilestoneActionPending: false,
|
||||
};
|
||||
|
||||
case types.VOTE_REFUND_PENDING:
|
||||
case types.WITHDRAW_REFUND_PENDING:
|
||||
return {
|
||||
...state,
|
||||
isRefundActionPending: true,
|
||||
refundActionError: null,
|
||||
};
|
||||
case types.VOTE_REFUND_FULFILLED:
|
||||
case types.WITHDRAW_REFUND_FULFILLED:
|
||||
return {
|
||||
...state,
|
||||
isRefundActionPending: false,
|
||||
};
|
||||
case types.VOTE_REFUND_REJECTED:
|
||||
case types.WITHDRAW_REFUND_REJECTED:
|
||||
return {
|
||||
...state,
|
||||
refundActionError: payload,
|
||||
isRefundActionPending: false,
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -35,6 +35,16 @@ enum web3Types {
|
|||
VOTE_AGAINST_MILESTONE_PAYOUT_REJECTED = 'VOTE_AGAINST_MILESTONE_PAYOUT_REJECTED',
|
||||
VOTE_AGAINST_MILESTONE_PAYOUT_PENDING = 'VOTE_AGAINST_MILESTONE_PAYOUT_PENDING',
|
||||
|
||||
VOTE_REFUND = 'VOTE_REFUND',
|
||||
VOTE_REFUND_FULFILLED = 'VOTE_REFUND_FULFILLED',
|
||||
VOTE_REFUND_REJECTED = 'VOTE_REFUND_REJECTED',
|
||||
VOTE_REFUND_PENDING = 'VOTE_REFUND_PENDING',
|
||||
|
||||
WITHDRAW_REFUND = 'WITHDRAW_REFUND',
|
||||
WITHDRAW_REFUND_FULFILLED = 'WITHDRAW_REFUND_FULFILLED',
|
||||
WITHDRAW_REFUND_REJECTED = 'WITHDRAW_REFUND_REJECTED',
|
||||
WITHDRAW_REFUND_PENDING = 'WITHDRAW_REFUND_PENDING',
|
||||
|
||||
ACCOUNTS = 'ACCOUNTS',
|
||||
ACCOUNTS_FULFILLED = 'ACCOUNTS_FULFILLED',
|
||||
ACCOUNTS_REJECTED = 'ACCOUNTS_REJECTED',
|
||||
|
|
|
@ -17,9 +17,11 @@ export async function getCrowdFundState(
|
|||
const isRaiseGoalReached = await crowdFundContract.methods
|
||||
.isRaiseGoalReached()
|
||||
.call({ from: account });
|
||||
const funded = isRaiseGoalReached
|
||||
? target
|
||||
: await web3.eth.getBalance(crowdFundContract._address);
|
||||
const balance = await web3.eth.getBalance(crowdFundContract._address);
|
||||
const funded = isRaiseGoalReached ? target : balance;
|
||||
const amountVotingForRefund = isRaiseGoalReached
|
||||
? await crowdFundContract.methods.amountVotingForRefund().call({ from: account })
|
||||
: '0';
|
||||
|
||||
const isFrozen = await crowdFundContract.methods.frozen().call({ from: account });
|
||||
const trustees = await collectArrayElements<string>(
|
||||
|
@ -115,8 +117,13 @@ export async function getCrowdFundState(
|
|||
|
||||
return {
|
||||
immediateFirstMilestonePayout,
|
||||
// TODO: Bignumber these 4
|
||||
balance: parseFloat(web3.utils.fromWei(String(balance), 'ether')),
|
||||
funded: parseFloat(web3.utils.fromWei(String(funded), 'ether')),
|
||||
target: parseFloat(web3.utils.fromWei(String(target), 'ether')),
|
||||
amountVotingForRefund: parseFloat(
|
||||
web3.utils.fromWei(String(amountVotingForRefund), 'ether'),
|
||||
),
|
||||
beneficiary,
|
||||
deadline,
|
||||
trustees,
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"tsc": "tsc",
|
||||
"link-contracts": "cd client/lib && ln -s ../../build/contracts contracts",
|
||||
"ganache": "ganache-cli -b 5",
|
||||
"truffle": "truffle exec ./bin/init-truffle.js && truffle console"
|
||||
"truffle": "truffle exec ./bin/init-truffle.js && cd client/lib/contracts && truffle console"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
|
Loading…
Reference in New Issue