2018-11-14 08:43:00 -08:00
|
|
|
|
import { ProposalDraft, CreateMilestone } from 'types';
|
2018-10-04 21:27:02 -07:00
|
|
|
|
import { TeamMember } from 'types';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import { isValidEthAddress, getAmountError } from 'utils/validators';
|
2018-10-04 21:27:02 -07:00
|
|
|
|
import { MILESTONE_STATE, ProposalWithCrowdFund } from 'types';
|
2018-11-14 08:43:00 -08:00
|
|
|
|
import { ProposalContractData } from 'modules/web3/actions';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import { Wei, toWei } from 'utils/units';
|
2018-10-19 15:03:37 -07:00
|
|
|
|
import { ONE_DAY } from 'utils/time';
|
|
|
|
|
import { PROPOSAL_CATEGORY } from 'api/constants';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
|
|
|
|
// TODO: Raise this limit
|
|
|
|
|
export const TARGET_ETH_LIMIT = 10;
|
|
|
|
|
|
|
|
|
|
interface CreateFormErrors {
|
|
|
|
|
title?: string;
|
|
|
|
|
brief?: string;
|
|
|
|
|
category?: string;
|
2018-11-14 08:43:00 -08:00
|
|
|
|
target?: string;
|
2018-09-27 13:25:49 -07:00
|
|
|
|
team?: string[];
|
2018-09-20 11:58:47 -07:00
|
|
|
|
details?: string;
|
2018-11-14 08:43:00 -08:00
|
|
|
|
payoutAddress?: string;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
trustees?: string[];
|
|
|
|
|
milestones?: string[];
|
2018-11-14 08:43:00 -08:00
|
|
|
|
deadlineDuration?: string;
|
|
|
|
|
voteDuration?: string;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-14 08:43:00 -08:00
|
|
|
|
export type KeyOfForm = keyof Partial<ProposalDraft>;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
export const FIELD_NAME_MAP: { [key in KeyOfForm]: string } = {
|
|
|
|
|
title: 'Title',
|
|
|
|
|
brief: 'Brief',
|
|
|
|
|
category: 'Category',
|
2018-11-14 08:43:00 -08:00
|
|
|
|
target: 'Target amount',
|
2018-09-27 13:25:49 -07:00
|
|
|
|
team: 'Team',
|
2018-09-20 11:58:47 -07:00
|
|
|
|
details: 'Details',
|
2018-11-14 08:43:00 -08:00
|
|
|
|
payoutAddress: 'Payout address',
|
2018-09-20 11:58:47 -07:00
|
|
|
|
trustees: 'Trustees',
|
|
|
|
|
milestones: 'Milestones',
|
2018-11-14 08:43:00 -08:00
|
|
|
|
deadlineDuration: 'Funding deadline',
|
|
|
|
|
voteDuration: 'Milestone deadline',
|
|
|
|
|
// Unused, but required by the type definition
|
|
|
|
|
proposalId: '',
|
|
|
|
|
dateCreated: '',
|
|
|
|
|
stage: '',
|
2018-09-20 11:58:47 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function getCreateErrors(
|
2018-11-14 08:43:00 -08:00
|
|
|
|
form: Partial<ProposalDraft>,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
skipRequired?: boolean,
|
|
|
|
|
): CreateFormErrors {
|
|
|
|
|
const errors: CreateFormErrors = {};
|
2018-11-14 08:43:00 -08:00
|
|
|
|
const { title, team, milestones, target, payoutAddress, trustees } = form;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
|
|
|
|
// Required fields with no extra validation
|
|
|
|
|
if (!skipRequired) {
|
2018-10-19 15:03:37 -07:00
|
|
|
|
for (const key in form) {
|
|
|
|
|
if (!form[key as KeyOfForm]) {
|
2018-11-14 08:43:00 -08:00
|
|
|
|
(errors as any)[key as KeyOfForm] = `${
|
|
|
|
|
FIELD_NAME_MAP[key as KeyOfForm]
|
|
|
|
|
} is required`;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
2018-10-19 15:03:37 -07:00
|
|
|
|
}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
|
|
|
|
if (!milestones || !milestones.length) {
|
|
|
|
|
errors.milestones = ['Must have at least one milestone'];
|
|
|
|
|
}
|
2018-09-27 13:25:49 -07:00
|
|
|
|
if (!team || !team.length) {
|
|
|
|
|
errors.team = ['Must have at least one team member'];
|
|
|
|
|
}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Title
|
2018-10-19 15:03:37 -07:00
|
|
|
|
if (title && title.length > 60) {
|
2018-09-20 11:58:47 -07:00
|
|
|
|
errors.title = 'Title can only be 60 characters maximum';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Amount to raise
|
2018-11-14 08:43:00 -08:00
|
|
|
|
const targetFloat = target ? parseFloat(target) : 0;
|
|
|
|
|
if (target && !Number.isNaN(targetFloat)) {
|
|
|
|
|
const targetErr = getAmountError(targetFloat, TARGET_ETH_LIMIT);
|
|
|
|
|
if (targetErr) {
|
|
|
|
|
errors.target = targetErr;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Payout address
|
2018-11-14 08:43:00 -08:00
|
|
|
|
if (payoutAddress && !isValidEthAddress(payoutAddress)) {
|
|
|
|
|
errors.payoutAddress = 'That doesn’t look like a valid address';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Trustees
|
2018-10-19 15:03:37 -07:00
|
|
|
|
if (trustees) {
|
|
|
|
|
let didTrusteeError = false;
|
|
|
|
|
const trusteeErrors = trustees.map((address, idx) => {
|
|
|
|
|
if (!address) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
2018-10-19 15:03:37 -07:00
|
|
|
|
let err = '';
|
|
|
|
|
if (!isValidEthAddress(address)) {
|
|
|
|
|
err = 'That doesn’t look like a valid address';
|
|
|
|
|
} else if (trustees.indexOf(address) !== idx) {
|
|
|
|
|
err = 'That address is already a trustee';
|
2018-11-14 08:43:00 -08:00
|
|
|
|
} else if (payoutAddress === address) {
|
2018-10-19 15:03:37 -07:00
|
|
|
|
err = 'That address is already a trustee';
|
|
|
|
|
}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
2018-10-19 15:03:37 -07:00
|
|
|
|
didTrusteeError = didTrusteeError || !!err;
|
|
|
|
|
return err;
|
|
|
|
|
});
|
|
|
|
|
if (didTrusteeError) {
|
|
|
|
|
errors.trustees = trusteeErrors;
|
|
|
|
|
}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Milestones
|
2018-10-19 15:03:37 -07:00
|
|
|
|
if (milestones) {
|
|
|
|
|
let didMilestoneError = false;
|
|
|
|
|
let cumulativeMilestonePct = 0;
|
|
|
|
|
const milestoneErrors = milestones.map((ms, idx) => {
|
2018-11-14 08:43:00 -08:00
|
|
|
|
if (!ms.title || !ms.content || !ms.dateEstimated || !ms.payoutPercent) {
|
2018-10-19 15:03:37 -07:00
|
|
|
|
didMilestoneError = true;
|
|
|
|
|
return '';
|
|
|
|
|
}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
2018-10-19 15:03:37 -07:00
|
|
|
|
let err = '';
|
|
|
|
|
if (ms.title.length > 40) {
|
|
|
|
|
err = 'Title length can only be 40 characters maximum';
|
2018-11-14 08:43:00 -08:00
|
|
|
|
} else if (ms.content.length > 200) {
|
2018-10-19 15:03:37 -07:00
|
|
|
|
err = 'Description can only be 200 characters maximum';
|
|
|
|
|
}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
2018-10-19 15:03:37 -07:00
|
|
|
|
// Last one shows percentage errors
|
2018-11-14 08:43:00 -08:00
|
|
|
|
cumulativeMilestonePct += parseInt(ms.payoutPercent, 10);
|
2018-10-19 15:03:37 -07:00
|
|
|
|
if (idx === milestones.length - 1 && cumulativeMilestonePct !== 100) {
|
|
|
|
|
err = `Payout percentages doesn’t add up to 100% (currently ${cumulativeMilestonePct}%)`;
|
|
|
|
|
}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
2018-10-19 15:03:37 -07:00
|
|
|
|
didMilestoneError = didMilestoneError || !!err;
|
|
|
|
|
return err;
|
|
|
|
|
});
|
|
|
|
|
if (didMilestoneError) {
|
|
|
|
|
errors.milestones = milestoneErrors;
|
|
|
|
|
}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-27 13:25:49 -07:00
|
|
|
|
// Team
|
2018-10-19 15:03:37 -07:00
|
|
|
|
if (team) {
|
|
|
|
|
let didTeamError = false;
|
|
|
|
|
const teamErrors = team.map(u => {
|
|
|
|
|
if (!u.name || !u.title || !u.emailAddress || !u.ethAddress) {
|
|
|
|
|
didTeamError = true;
|
|
|
|
|
return '';
|
|
|
|
|
}
|
2018-09-27 13:25:49 -07:00
|
|
|
|
|
2018-10-19 15:03:37 -07:00
|
|
|
|
const err = getCreateTeamMemberError(u);
|
|
|
|
|
didTeamError = didTeamError || !!err;
|
|
|
|
|
return err;
|
|
|
|
|
});
|
|
|
|
|
if (didTeamError) {
|
|
|
|
|
errors.team = teamErrors;
|
|
|
|
|
}
|
2018-09-27 13:25:49 -07:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-20 11:58:47 -07:00
|
|
|
|
return errors;
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-27 13:25:49 -07:00
|
|
|
|
export function getCreateTeamMemberError(user: TeamMember) {
|
|
|
|
|
if (user.name.length > 30) {
|
|
|
|
|
return 'Display name can only be 30 characters maximum';
|
|
|
|
|
} else if (user.title.length > 30) {
|
|
|
|
|
return 'Title can only be 30 characters maximum';
|
|
|
|
|
} else if (!/.+\@.+\..+/.test(user.emailAddress)) {
|
|
|
|
|
return 'That doesn’t look like a valid email address';
|
|
|
|
|
} else if (!isValidEthAddress(user.ethAddress)) {
|
|
|
|
|
return 'That doesn’t look like a valid ETH address';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-04 21:27:02 -07:00
|
|
|
|
function milestoneToMilestoneAmount(milestone: CreateMilestone, raiseGoal: Wei) {
|
2018-11-14 08:43:00 -08:00
|
|
|
|
return raiseGoal.divn(100).mul(Wei(milestone.payoutPercent));
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-14 08:43:00 -08:00
|
|
|
|
export function proposalToContractData(form: ProposalDraft): ProposalContractData {
|
|
|
|
|
const targetInWei = toWei(form.target, 'ether');
|
2018-09-20 11:58:47 -07:00
|
|
|
|
const milestoneAmounts = form.milestones.map(m =>
|
|
|
|
|
milestoneToMilestoneAmount(m, targetInWei),
|
|
|
|
|
);
|
2018-10-19 15:03:37 -07:00
|
|
|
|
const immediateFirstMilestonePayout = form.milestones[0]!.immediatePayout;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ethAmount: targetInWei,
|
2018-11-14 08:43:00 -08:00
|
|
|
|
payoutAddress: form.payoutAddress,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
trusteesAddresses: form.trustees,
|
|
|
|
|
milestoneAmounts,
|
2018-11-14 08:43:00 -08:00
|
|
|
|
durationInMinutes: form.deadlineDuration || ONE_DAY * 60,
|
|
|
|
|
milestoneVotingPeriodInMinutes: form.voteDuration || ONE_DAY * 7,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
immediateFirstMilestonePayout,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This is kind of a disgusting function, sorry.
|
2018-11-14 08:43:00 -08:00
|
|
|
|
export function makeProposalPreviewFromDraft(
|
|
|
|
|
draft: ProposalDraft,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
): ProposalWithCrowdFund {
|
2018-11-14 08:43:00 -08:00
|
|
|
|
const target = parseFloat(draft.target);
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
|
|
|
|
return {
|
2018-11-07 09:33:19 -08:00
|
|
|
|
proposalId: 0,
|
|
|
|
|
proposalUrlId: '0-title',
|
|
|
|
|
proposalAddress: '0x0',
|
2018-09-20 11:58:47 -07:00
|
|
|
|
dateCreated: Date.now(),
|
2018-11-14 08:43:00 -08:00
|
|
|
|
title: draft.title,
|
|
|
|
|
body: draft.details,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
stage: 'preview',
|
2018-11-14 08:43:00 -08:00
|
|
|
|
category: draft.category || PROPOSAL_CATEGORY.DAPP,
|
|
|
|
|
team: draft.team,
|
|
|
|
|
milestones: draft.milestones.map((m, idx) => ({
|
2018-09-20 11:58:47 -07:00
|
|
|
|
index: idx,
|
|
|
|
|
title: m.title,
|
2018-11-14 08:43:00 -08:00
|
|
|
|
body: m.content,
|
|
|
|
|
content: m.content,
|
|
|
|
|
amount: toWei(target * (parseInt(m.payoutPercent, 10) / 100), 'ether'),
|
2018-09-20 11:58:47 -07:00
|
|
|
|
amountAgainstPayout: Wei('0'),
|
|
|
|
|
percentAgainstPayout: 0,
|
|
|
|
|
payoutRequestVoteDeadline: Date.now(),
|
2018-11-14 08:43:00 -08:00
|
|
|
|
dateEstimated: m.dateEstimated,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
immediatePayout: m.immediatePayout,
|
|
|
|
|
isImmediatePayout: m.immediatePayout,
|
|
|
|
|
isPaid: false,
|
|
|
|
|
payoutPercent: m.payoutPercent.toString(),
|
|
|
|
|
state: MILESTONE_STATE.WAITING,
|
|
|
|
|
stage: MILESTONE_STATE.WAITING,
|
|
|
|
|
})),
|
|
|
|
|
crowdFund: {
|
2018-11-14 08:43:00 -08:00
|
|
|
|
immediateFirstMilestonePayout: draft.milestones[0].immediatePayout,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
balance: Wei('0'),
|
|
|
|
|
funded: Wei('0'),
|
|
|
|
|
percentFunded: 0,
|
|
|
|
|
target: toWei(target, 'ether'),
|
|
|
|
|
amountVotingForRefund: Wei('0'),
|
|
|
|
|
percentVotingForRefund: 0,
|
2018-11-14 08:43:00 -08:00
|
|
|
|
beneficiary: draft.payoutAddress,
|
|
|
|
|
trustees: draft.trustees,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
deadline: Date.now() + 100000,
|
|
|
|
|
contributors: [],
|
|
|
|
|
milestones: [],
|
|
|
|
|
milestoneVotingPeriod: 0,
|
|
|
|
|
isFrozen: false,
|
|
|
|
|
isRaiseGoalReached: false,
|
|
|
|
|
},
|
|
|
|
|
crowdFundContract: null,
|
|
|
|
|
};
|
|
|
|
|
}
|