2018-11-14 08:43:00 -08:00
|
|
|
|
import { ProposalDraft, CreateMilestone } from 'types';
|
2018-11-16 15:05:17 -08:00
|
|
|
|
import { User } from 'types';
|
2018-12-14 11:36:22 -08:00
|
|
|
|
import { getAmountError } from 'utils/validators';
|
2018-12-21 10:47:50 -08:00
|
|
|
|
import { MILESTONE_STATE, Proposal } from 'types';
|
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
|
2018-11-14 09:59:48 -08:00
|
|
|
|
export const TARGET_ETH_LIMIT = 1000;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
|
|
|
|
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-11-14 09:59:48 -08:00
|
|
|
|
content?: string;
|
2018-11-14 08:43:00 -08:00
|
|
|
|
payoutAddress?: string;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
milestones?: string[];
|
2018-11-14 08:43:00 -08:00
|
|
|
|
deadlineDuration?: string;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-14 09:59:48 -08:00
|
|
|
|
export type KeyOfForm = keyof CreateFormErrors;
|
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-11-14 09:59:48 -08:00
|
|
|
|
content: 'Details',
|
2018-11-14 08:43:00 -08:00
|
|
|
|
payoutAddress: 'Payout address',
|
2018-09-20 11:58:47 -07:00
|
|
|
|
milestones: 'Milestones',
|
2018-11-14 08:43:00 -08:00
|
|
|
|
deadlineDuration: 'Funding deadline',
|
2018-09-20 11:58:47 -07:00
|
|
|
|
};
|
|
|
|
|
|
2018-11-14 09:59:48 -08:00
|
|
|
|
const requiredFields = [
|
|
|
|
|
'title',
|
|
|
|
|
'brief',
|
|
|
|
|
'category',
|
|
|
|
|
'target',
|
|
|
|
|
'content',
|
|
|
|
|
'payoutAddress',
|
|
|
|
|
'deadlineDuration',
|
|
|
|
|
];
|
|
|
|
|
|
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-12-21 10:27:39 -08:00
|
|
|
|
const { title, team, milestones, target, payoutAddress } = form;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
|
|
|
|
// Required fields with no extra validation
|
|
|
|
|
if (!skipRequired) {
|
2018-11-14 09:59:48 -08:00
|
|
|
|
for (const key of requiredFields) {
|
2018-10-19 15:03:37 -07:00
|
|
|
|
if (!form[key as KeyOfForm]) {
|
2018-11-14 09:59:48 -08:00
|
|
|
|
errors[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-12-14 11:36:22 -08:00
|
|
|
|
if (!payoutAddress) {
|
2018-11-14 08:43:00 -08:00
|
|
|
|
errors.payoutAddress = 'That doesn’t look like a valid address';
|
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
|
|
|
|
}
|
|
|
|
|
return errors;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-16 15:05:17 -08:00
|
|
|
|
export function getCreateTeamMemberError(user: User) {
|
|
|
|
|
if (user.displayName.length > 30) {
|
2018-09-27 13:25:49 -07:00
|
|
|
|
return 'Display name can only be 30 characters maximum';
|
|
|
|
|
} else if (user.title.length > 30) {
|
|
|
|
|
return 'Title can only be 30 characters maximum';
|
2018-12-14 11:36:22 -08:00
|
|
|
|
} else if (!user.emailAddress || !/.+\@.+\..+/.test(user.emailAddress)) {
|
2018-09-27 13:25:49 -07:00
|
|
|
|
return 'That doesn’t look like a valid email address';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-16 08:57:03 -08:00
|
|
|
|
export function getCreateWarnings(form: Partial<ProposalDraft>): string[] {
|
|
|
|
|
const warnings = [];
|
|
|
|
|
|
|
|
|
|
// Warn about pending invites
|
2018-11-16 11:23:42 -08:00
|
|
|
|
const hasPending =
|
|
|
|
|
(form.invites || []).filter(inv => inv.accepted === null).length !== 0;
|
|
|
|
|
if (hasPending) {
|
2018-11-16 08:57:03 -08:00
|
|
|
|
warnings.push(`
|
|
|
|
|
You still have pending team invitations. If you publish before they
|
|
|
|
|
are accepted, your team will be locked in and they won’t be able to
|
|
|
|
|
accept join.
|
|
|
|
|
`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return warnings;
|
|
|
|
|
}
|
|
|
|
|
|
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-12-14 11:36:22 -08:00
|
|
|
|
export function proposalToContractData(form: ProposalDraft): any {
|
2018-11-14 08:43:00 -08:00
|
|
|
|
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-12-21 10:27:39 -08:00
|
|
|
|
trusteesAddresses: [],
|
2018-09-20 11:58:47 -07:00
|
|
|
|
milestoneAmounts,
|
2018-11-14 08:43:00 -08:00
|
|
|
|
durationInMinutes: form.deadlineDuration || ONE_DAY * 60,
|
2018-12-21 10:27:39 -08:00
|
|
|
|
milestoneVotingPeriodInMinutes: 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-12-21 10:47:50 -08:00
|
|
|
|
): Proposal {
|
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,
|
2018-11-16 15:05:17 -08:00
|
|
|
|
brief: draft.brief,
|
2018-11-14 09:59:48 -08:00
|
|
|
|
content: draft.content,
|
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
|
|
|
|
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,
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
}
|